feat!: added PDF and screenshot archive support
This commit is contained in:
parent
bd3b2f50f2
commit
0d5579b56d
|
@ -1,5 +1,3 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
|
@ -35,3 +33,6 @@ yarn-error.log*
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# generated files and folders
|
||||||
|
/data
|
||||||
|
|
|
@ -7,14 +7,14 @@ import { useRouter } from "next/router";
|
||||||
import { NewLink } from "@/types/global";
|
import { NewLink } from "@/types/global";
|
||||||
import useLinkSlice from "@/store/links";
|
import useLinkSlice from "@/store/links";
|
||||||
|
|
||||||
export default function () {
|
export default function ({ toggleLinkModal }: { toggleLinkModal: Function }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [newLink, setNewLink] = useState<NewLink>({
|
const [newLink, setNewLink] = useState<NewLink>({
|
||||||
name: "",
|
name: "",
|
||||||
url: "",
|
url: "",
|
||||||
tags: [],
|
tags: [],
|
||||||
collectionId: { id: Number(router.query.id) },
|
collection: { id: Number(router.query.id) },
|
||||||
});
|
});
|
||||||
|
|
||||||
const { addLink } = useLinkSlice();
|
const { addLink } = useLinkSlice();
|
||||||
|
@ -30,23 +30,13 @@ export default function () {
|
||||||
const setCollection = (e: any) => {
|
const setCollection = (e: any) => {
|
||||||
const collection = { id: e?.value, isNew: e?.__isNew__ };
|
const collection = { id: e?.value, isNew: e?.__isNew__ };
|
||||||
|
|
||||||
setNewLink({ ...newLink, collectionId: collection });
|
setNewLink({ ...newLink, collection: collection });
|
||||||
};
|
};
|
||||||
|
|
||||||
const postLink = async () => {
|
const submitLink = async () => {
|
||||||
const response = await fetch("/api/routes/links", {
|
const response = await addLink(newLink);
|
||||||
body: JSON.stringify(newLink),
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
method: "POST",
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
if (response) toggleLinkModal();
|
||||||
|
|
||||||
console.log(newLink);
|
|
||||||
|
|
||||||
console.log(data);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -87,7 +77,7 @@ export default function () {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="mx-auto mt-2 bg-sky-500 text-white flex items-center gap-2 py-2 px-5 rounded select-none font-bold cursor-pointer duration-100 hover:bg-sky-400"
|
className="mx-auto mt-2 bg-sky-500 text-white flex items-center gap-2 py-2 px-5 rounded select-none font-bold cursor-pointer duration-100 hover:bg-sky-400"
|
||||||
onClick={() => addLink(newLink)}
|
onClick={submitLink}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPlus} className="h-5" />
|
<FontAwesomeIcon icon={faPlus} className="h-5" />
|
||||||
Add Link
|
Add Link
|
||||||
|
|
|
@ -9,7 +9,7 @@ export default function ({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="border border-sky-100 mb-5 bg-gray-100 p-5 rounded">
|
<div className="border border-sky-100 mb-5 bg-gray-100 p-5 rounded">
|
||||||
<div className="flex items-baseline gap-1">
|
{/* <div className="flex items-baseline gap-1">
|
||||||
<p className="text-sm text-sky-600">{count + 1}.</p>
|
<p className="text-sm text-sky-600">{count + 1}.</p>
|
||||||
<p className="text-lg text-sky-500">{link.name}</p>
|
<p className="text-lg text-sky-500">{link.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,7 +17,8 @@ export default function ({
|
||||||
{link.tags.map((e, i) => (
|
{link.tags.map((e, i) => (
|
||||||
<p key={i}>{e.name}</p>
|
<p key={i}>{e.name}</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div> */}
|
||||||
|
{JSON.stringify(link)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,10 +59,10 @@ export default function () {
|
||||||
}
|
}
|
||||||
}, [router, collections, tags]);
|
}, [router, collections, tags]);
|
||||||
|
|
||||||
const [collectionInput, setCollectionInput] = useState(false);
|
const [linkModal, setLinkModal] = useState(false);
|
||||||
|
|
||||||
const toggleCollectionInput = () => {
|
const toggleLinkModal = () => {
|
||||||
setCollectionInput(!collectionInput);
|
setLinkModal(!linkModal);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -76,7 +76,7 @@ export default function () {
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faPlus}
|
icon={faPlus}
|
||||||
onClick={toggleCollectionInput}
|
onClick={toggleLinkModal}
|
||||||
className="select-none cursor-pointer w-5 h-5 text-white bg-sky-500 p-2 rounded hover:bg-sky-400 duration-100"
|
className="select-none cursor-pointer w-5 h-5 text-white bg-sky-500 p-2 rounded hover:bg-sky-400 duration-100"
|
||||||
/>
|
/>
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
|
@ -90,13 +90,13 @@ export default function () {
|
||||||
Sign Out
|
Sign Out
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{collectionInput ? (
|
{linkModal ? (
|
||||||
<div className="fixed top-0 bottom-0 right-0 left-0 bg-gray-500 bg-opacity-10 flex items-center fade-in">
|
<div className="fixed top-0 bottom-0 right-0 left-0 bg-gray-500 bg-opacity-10 flex items-center fade-in">
|
||||||
<ClickAwayHandler
|
<ClickAwayHandler
|
||||||
onClickOutside={toggleCollectionInput}
|
onClickOutside={toggleLinkModal}
|
||||||
className="w-fit mx-auto"
|
className="w-fit mx-auto"
|
||||||
>
|
>
|
||||||
<AddLinkModal />
|
<AddLinkModal toggleLinkModal={toggleLinkModal} />
|
||||||
</ClickAwayHandler>
|
</ClickAwayHandler>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { chromium, devices } from "playwright";
|
||||||
|
|
||||||
|
export default async (url: string, collectionId: number, linkId: number) => {
|
||||||
|
const archivePath = `data/archives/${collectionId}/${linkId}`;
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const context = await browser.newContext(devices["Desktop Chrome"]);
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
// const contexts = browser.contexts();
|
||||||
|
// console.log(contexts.length);
|
||||||
|
|
||||||
|
await page.goto(url);
|
||||||
|
|
||||||
|
await page.pdf({ path: archivePath + ".pdf" });
|
||||||
|
|
||||||
|
await page.screenshot({ fullPage: true, path: archivePath + ".png" });
|
||||||
|
|
||||||
|
await context.close();
|
||||||
|
await browser.close();
|
||||||
|
};
|
|
@ -1,6 +1,7 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
import { Session } from "next-auth";
|
import { Session } from "next-auth";
|
||||||
|
import { existsSync, mkdirSync } from "fs";
|
||||||
|
|
||||||
export default async function (
|
export default async function (
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
|
@ -39,7 +40,7 @@ export default async function (
|
||||||
return res.status(400).json({ response: "Collection already exists." });
|
return res.status(400).json({ response: "Collection already exists." });
|
||||||
}
|
}
|
||||||
|
|
||||||
const createCollection = await prisma.collection.create({
|
const newCollection = await prisma.collection.create({
|
||||||
data: {
|
data: {
|
||||||
owner: {
|
owner: {
|
||||||
connect: {
|
connect: {
|
||||||
|
@ -50,7 +51,11 @@ export default async function (
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const collectionPath = `data/archives/${newCollection.id}`;
|
||||||
|
if (!existsSync(collectionPath))
|
||||||
|
mkdirSync(collectionPath, { recursive: true });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
response: createCollection,
|
response: newCollection,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,11 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
import { Session } from "next-auth";
|
import { Session } from "next-auth";
|
||||||
import { LinkAndTags, NewLink } from "@/types/global";
|
import { LinkAndTags, NewLink } from "@/types/global";
|
||||||
|
import { existsSync, mkdirSync } from "fs";
|
||||||
|
import getTitle from "../../getTitle";
|
||||||
|
import archive from "../../archive";
|
||||||
|
import { Link } from "@prisma/client";
|
||||||
|
import AES from "crypto-js/aes";
|
||||||
|
|
||||||
export default async function (
|
export default async function (
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
|
@ -21,8 +26,8 @@ export default async function (
|
||||||
.json({ response: "Please enter a valid name for the link." });
|
.json({ response: "Please enter a valid name for the link." });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (link.collectionId.isNew) {
|
if (link.collection.isNew) {
|
||||||
const collectionId = link.collectionId.id as string;
|
const collectionId = link.collection.id as string;
|
||||||
|
|
||||||
const findCollection = await prisma.user.findFirst({
|
const findCollection = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
@ -43,7 +48,7 @@ export default async function (
|
||||||
return res.status(400).json({ response: "Collection already exists." });
|
return res.status(400).json({ response: "Collection already exists." });
|
||||||
}
|
}
|
||||||
|
|
||||||
const createCollection = await prisma.collection.create({
|
const newCollection = await prisma.collection.create({
|
||||||
data: {
|
data: {
|
||||||
owner: {
|
owner: {
|
||||||
connect: {
|
connect: {
|
||||||
|
@ -54,12 +59,18 @@ export default async function (
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
link.collectionId.id = createCollection.id;
|
const collectionPath = `data/archives/${newCollection.id}`;
|
||||||
|
if (!existsSync(collectionPath))
|
||||||
|
mkdirSync(collectionPath, { recursive: true });
|
||||||
|
|
||||||
|
link.collection.id = newCollection.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const collectionId = link.collectionId.id as number;
|
const collectionId = link.collection.id as number;
|
||||||
|
|
||||||
const createLink: LinkAndTags = await prisma.link.create({
|
const title = await getTitle(link.url);
|
||||||
|
|
||||||
|
const newLink: Link = await prisma.link.create({
|
||||||
data: {
|
data: {
|
||||||
name: link.name,
|
name: link.name,
|
||||||
url: link.url,
|
url: link.url,
|
||||||
|
@ -86,11 +97,34 @@ export default async function (
|
||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
title,
|
||||||
|
isFavorites: false,
|
||||||
|
screenshotPath: "",
|
||||||
|
pdfPath: "",
|
||||||
},
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const AES_SECRET = process.env.AES_SECRET as string;
|
||||||
|
|
||||||
|
const screenShotHashedPath = AES.encrypt(
|
||||||
|
`data/archives/${newLink.collectionId}/${newLink.id}.png`,
|
||||||
|
AES_SECRET
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
const pdfHashedPath = AES.encrypt(
|
||||||
|
`data/archives/${newLink.collectionId}/${newLink.id}.pdf`,
|
||||||
|
AES_SECRET
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
const updatedLink: LinkAndTags = await prisma.link.update({
|
||||||
|
where: { id: newLink.id },
|
||||||
|
data: { screenshotPath: screenShotHashedPath, pdfPath: pdfHashedPath },
|
||||||
include: { tags: true },
|
include: { tags: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
archive(updatedLink.url, updatedLink.collectionId, updatedLink.id);
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
response: createLink,
|
response: updatedLink,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
export default async (url: string) => {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const text = await response.text();
|
||||||
|
|
||||||
|
// regular expression to find the <title> tag
|
||||||
|
let match = text.match(/<title.*>([^<]*)<\/title>/);
|
||||||
|
if (match) return match[1];
|
||||||
|
else return "";
|
||||||
|
};
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { prisma } from "@/lib/api/db";
|
||||||
|
|
||||||
|
export default async (userId: number, collectionId: number) => {
|
||||||
|
const check: any = await prisma.collection.findFirst({
|
||||||
|
where: {
|
||||||
|
AND: {
|
||||||
|
id: collectionId,
|
||||||
|
OR: [{ ownerId: userId }, { members: { some: { userId } } }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: { members: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return check;
|
||||||
|
};
|
|
@ -19,14 +19,17 @@
|
||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@next/font": "13.1.6",
|
"@next/font": "13.1.6",
|
||||||
"@prisma/client": "^4.9.0",
|
"@prisma/client": "^4.9.0",
|
||||||
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.11.18",
|
||||||
"@types/react": "18.0.27",
|
"@types/react": "18.0.27",
|
||||||
"@types/react-dom": "18.0.10",
|
"@types/react-dom": "18.0.10",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
"eslint": "8.33.0",
|
"eslint": "8.33.0",
|
||||||
"eslint-config-next": "13.1.6",
|
"eslint-config-next": "13.1.6",
|
||||||
"next": "13.1.6",
|
"next": "13.1.6",
|
||||||
"next-auth": "^4.19.1",
|
"next-auth": "^4.19.1",
|
||||||
|
"playwright": "^1.31.2",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-select": "^5.7.0",
|
"react-select": "^5.7.0",
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { getServerSession } from "next-auth/next";
|
||||||
|
import { authOptions } from "pages/api/auth/[...nextauth]";
|
||||||
|
import AES from "crypto-js/aes";
|
||||||
|
import enc from "crypto-js/enc-utf8";
|
||||||
|
import path from "path";
|
||||||
|
import fs from "fs";
|
||||||
|
import hasAccessToCollection from "@/lib/api/hasAccessToCollection";
|
||||||
|
|
||||||
|
export default async function (req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (!req.query.params)
|
||||||
|
return res.status(401).json({ response: "Invalid parameters." });
|
||||||
|
|
||||||
|
const collectionId = req.query.params[0];
|
||||||
|
|
||||||
|
const session = await getServerSession(req, res, authOptions);
|
||||||
|
|
||||||
|
if (!session?.user?.email)
|
||||||
|
return res.status(401).json({ response: "You must be logged in." });
|
||||||
|
|
||||||
|
const collectionIsAccessible = await hasAccessToCollection(
|
||||||
|
session.user.id,
|
||||||
|
Number(collectionId)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!collectionIsAccessible)
|
||||||
|
return res
|
||||||
|
.status(401)
|
||||||
|
.json({ response: "You don't have access to this collection." });
|
||||||
|
|
||||||
|
const AES_SECRET = process.env.AES_SECRET as string;
|
||||||
|
const encryptedPath = decodeURIComponent(req.query.params[1]) as string;
|
||||||
|
const decryptedPath = AES.decrypt(encryptedPath, AES_SECRET).toString(enc);
|
||||||
|
|
||||||
|
const filePath = path.join(process.cwd(), decryptedPath);
|
||||||
|
const file = fs.readFileSync(filePath);
|
||||||
|
|
||||||
|
if (filePath.endsWith(".pdf"))
|
||||||
|
res.setHeader("Content-Type", "application/pdf");
|
||||||
|
|
||||||
|
if (filePath.endsWith(".png")) res.setHeader("Content-Type", "image/png");
|
||||||
|
|
||||||
|
return res.status(200).send(file);
|
||||||
|
}
|
|
@ -37,17 +37,11 @@ export default async function (
|
||||||
name: body.name,
|
name: body.name,
|
||||||
email: body.email,
|
email: body.email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
collections: {
|
|
||||||
create: {
|
|
||||||
name: "First Collection",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(201).json({ message: "User successfully created." });
|
res.status(201).json({ message: "User successfully created." });
|
||||||
} else if (checkIfUserExists) {
|
} else if (checkIfUserExists) {
|
||||||
console.log(checkIfUserExists);
|
|
||||||
res.status(400).json({ message: "User already exists." });
|
res.status(400).json({ message: "User already exists." });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ CREATE TABLE "UsersAndCollections" (
|
||||||
"userId" INTEGER NOT NULL,
|
"userId" INTEGER NOT NULL,
|
||||||
"collectionId" INTEGER NOT NULL,
|
"collectionId" INTEGER NOT NULL,
|
||||||
"canCreate" BOOLEAN NOT NULL,
|
"canCreate" BOOLEAN NOT NULL,
|
||||||
"canRead" BOOLEAN NOT NULL,
|
|
||||||
"canUpdate" BOOLEAN NOT NULL,
|
"canUpdate" BOOLEAN NOT NULL,
|
||||||
"canDelete" BOOLEAN NOT NULL,
|
"canDelete" BOOLEAN NOT NULL,
|
||||||
|
|
|
@ -36,7 +36,6 @@ model UsersAndCollections {
|
||||||
collectionId Int
|
collectionId Int
|
||||||
|
|
||||||
canCreate Boolean
|
canCreate Boolean
|
||||||
canRead Boolean
|
|
||||||
canUpdate Boolean
|
canUpdate Boolean
|
||||||
canDelete Boolean
|
canDelete Boolean
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { LinkAndTags, NewLink } from "@/types/global";
|
||||||
type LinkSlice = {
|
type LinkSlice = {
|
||||||
links: LinkAndTags[];
|
links: LinkAndTags[];
|
||||||
setLinks: () => void;
|
setLinks: () => void;
|
||||||
addLink: (linkName: NewLink) => void;
|
addLink: (linkName: NewLink) => Promise<boolean>;
|
||||||
updateLink: (link: LinkAndTags) => void;
|
updateLink: (link: LinkAndTags) => void;
|
||||||
removeLink: (linkId: number) => void;
|
removeLink: (linkId: number) => void;
|
||||||
};
|
};
|
||||||
|
@ -33,6 +33,8 @@ const useLinkSlice = create<LinkSlice>()((set) => ({
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
links: [...state.links, data.response],
|
links: [...state.links, data.response],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
return response.ok;
|
||||||
},
|
},
|
||||||
updateLink: (link) =>
|
updateLink: (link) =>
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
|
|
|
@ -8,7 +8,7 @@ export interface NewLink {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
collectionId: {
|
collection: {
|
||||||
id: string | number;
|
id: string | number;
|
||||||
isNew?: boolean;
|
isNew?: boolean;
|
||||||
};
|
};
|
||||||
|
|
22
yarn.lock
22
yarn.lock
|
@ -396,6 +396,11 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/crypto-js@^4.1.1":
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d"
|
||||||
|
integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==
|
||||||
|
|
||||||
"@types/json5@^0.0.29":
|
"@types/json5@^0.0.29":
|
||||||
version "0.0.29"
|
version "0.0.29"
|
||||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||||
|
@ -868,6 +873,11 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3:
|
||||||
shebang-command "^2.0.0"
|
shebang-command "^2.0.0"
|
||||||
which "^2.0.1"
|
which "^2.0.1"
|
||||||
|
|
||||||
|
crypto-js@^4.1.1:
|
||||||
|
version "4.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf"
|
||||||
|
integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==
|
||||||
|
|
||||||
cssesc@^3.0.0:
|
cssesc@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
|
||||||
|
@ -2373,6 +2383,18 @@ pify@^2.3.0:
|
||||||
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||||
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
|
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
|
||||||
|
|
||||||
|
playwright-core@1.31.2:
|
||||||
|
version "1.31.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.31.2.tgz#debf4b215d14cb619adb7e511c164d068075b2ed"
|
||||||
|
integrity sha512-a1dFgCNQw4vCsG7bnojZjDnPewZcw7tZUNFN0ZkcLYKj+mPmXvg4MpaaKZ5SgqPsOmqIf2YsVRkgqiRDxD+fDQ==
|
||||||
|
|
||||||
|
playwright@^1.31.2:
|
||||||
|
version "1.31.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.31.2.tgz#4252280586c596746122cd1fdf9f8ff6a63fa852"
|
||||||
|
integrity sha512-jpC47n2PKQNtzB7clmBuWh6ftBRS/Bt5EGLigJ9k2QAKcNeYXZkEaDH5gmvb6+AbcE0DO6GnXdbl9ogG6Eh+og==
|
||||||
|
dependencies:
|
||||||
|
playwright-core "1.31.2"
|
||||||
|
|
||||||
postcss-import@^14.1.0:
|
postcss-import@^14.1.0:
|
||||||
version "14.1.0"
|
version "14.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0"
|
resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0"
|
||||||
|
|
Ŝarĝante…
Reference in New Issue