mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2025-04-26 22:28:30 -04:00
only reveal a db record via /api/hits
if it matches a real page
This commit is contained in:
parent
8d47958473
commit
155c6cacd9
@ -1,9 +1,7 @@
|
|||||||
// Next.js constants (not needed in frontend)
|
// Next.js constants (not needed in frontend)
|
||||||
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
// directory containing .mdx files relative to project root
|
// directory containing .mdx files relative to project root
|
||||||
export const NOTES_DIR = path.join(process.cwd(), "notes");
|
export const NOTES_DIR = "notes";
|
||||||
|
|
||||||
// normalize the timestamp saved when building/deploying (see next.config.js) and fall back to right now:
|
// normalize the timestamp saved when building/deploying (see next.config.js) and fall back to right now:
|
||||||
export const RELEASE_DATE = new Date(process.env.RELEASE_DATE || Date.now()).toISOString();
|
export const RELEASE_DATE = new Date(process.env.RELEASE_DATE || Date.now()).toISOString();
|
||||||
|
@ -3,8 +3,7 @@ import { getAllNotes } from "./parse-notes";
|
|||||||
import * as config from "../config";
|
import * as config from "../config";
|
||||||
import { RELEASE_DATE } from "../config/constants";
|
import { RELEASE_DATE } from "../config/constants";
|
||||||
import { favicons } from "../config/seo";
|
import { favicons } from "../config/seo";
|
||||||
import type { GetServerSidePropsContext, GetServerSidePropsResult, PreviewData } from "next";
|
import type { GetServerSideProps } from "next";
|
||||||
import type { ParsedUrlQuery } from "querystring";
|
|
||||||
|
|
||||||
export type BuildFeedOptions = {
|
export type BuildFeedOptions = {
|
||||||
edgeCacheAge?: number; // in seconds, defaults to 43200 (12 hours)
|
edgeCacheAge?: number; // in seconds, defaults to 43200 (12 hours)
|
||||||
@ -13,10 +12,10 @@ export type BuildFeedOptions = {
|
|||||||
// handles literally *everything* about building the server-side rss/atom feeds and writing the response.
|
// handles literally *everything* about building the server-side rss/atom feeds and writing the response.
|
||||||
// all the page needs to do is `return buildFeed(context, "rss")` from getServerSideProps.
|
// all the page needs to do is `return buildFeed(context, "rss")` from getServerSideProps.
|
||||||
export const buildFeed = async (
|
export const buildFeed = async (
|
||||||
context: GetServerSidePropsContext<ParsedUrlQuery, PreviewData>,
|
context: Parameters<GetServerSideProps>[0],
|
||||||
type: "rss" | "atom" | "json",
|
type: "rss" | "atom" | "json",
|
||||||
options?: BuildFeedOptions
|
options?: BuildFeedOptions
|
||||||
): Promise<GetServerSidePropsResult<Record<string, never>>> => {
|
): Promise<ReturnType<GetServerSideProps<Record<string, never>>>> => {
|
||||||
const { res } = context;
|
const { res } = context;
|
||||||
|
|
||||||
// https://github.com/jpmonette/feed#example
|
// https://github.com/jpmonette/feed#example
|
||||||
|
@ -21,13 +21,13 @@ const IsomorphicDayJs = (date?: dayjs.ConfigType): dayjs.Dayjs => {
|
|||||||
// simple wrapper around dayjs.format() to normalize timezone across the site, both server and client side, to prevent
|
// simple wrapper around dayjs.format() to normalize timezone across the site, both server and client side, to prevent
|
||||||
// hydration errors by returning an instance of dayjs with these defaults set.
|
// hydration errors by returning an instance of dayjs with these defaults set.
|
||||||
// date defaults to now, format defaults to ISO 8601 (e.g. 2022-04-07T21:53:33-04:00)
|
// date defaults to now, format defaults to ISO 8601 (e.g. 2022-04-07T21:53:33-04:00)
|
||||||
export const formatDate = (date?: dayjs.ConfigType, formatStr?: string) => {
|
export const formatDate = (date?: dayjs.ConfigType, formatStr?: string): string => {
|
||||||
return IsomorphicDayJs(date).tz(timeZone).format(formatStr);
|
return IsomorphicDayJs(date).tz(timeZone).format(formatStr);
|
||||||
};
|
};
|
||||||
|
|
||||||
// returns the human-friendly difference between now and given date (e.g. "5 minutes", "9 months", etc.)
|
// returns the human-friendly difference between now and given date (e.g. "5 minutes", "9 months", etc.)
|
||||||
// set `{ suffix: true }` to include the "... ago" or "in ..." for past/future
|
// set `{ suffix: true }` to include the "... ago" or "in ..." for past/future
|
||||||
export const formatTimeAgo = (date: dayjs.ConfigType, options?: { suffix?: boolean }) => {
|
export const formatTimeAgo = (date: dayjs.ConfigType, options?: { suffix?: boolean }): string => {
|
||||||
return IsomorphicDayJs().isBefore(date)
|
return IsomorphicDayJs().isBefore(date)
|
||||||
? IsomorphicDayJs(date).toNow(!options?.suffix)
|
? IsomorphicDayJs(date).toNow(!options?.suffix)
|
||||||
: IsomorphicDayJs(date).fromNow(!options?.suffix);
|
: IsomorphicDayJs(date).fromNow(!options?.suffix);
|
||||||
|
@ -14,7 +14,7 @@ import type { NoteFrontMatter } from "../../types";
|
|||||||
export const getNoteSlugs = async (): Promise<string[]> => {
|
export const getNoteSlugs = async (): Promise<string[]> => {
|
||||||
// list all .mdx files in NOTES_DIR
|
// list all .mdx files in NOTES_DIR
|
||||||
const mdxFiles = await glob("*.mdx", {
|
const mdxFiles = await glob("*.mdx", {
|
||||||
cwd: NOTES_DIR,
|
cwd: path.join(process.cwd(), NOTES_DIR),
|
||||||
dot: false,
|
dot: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ export const getNoteData = async (
|
|||||||
frontMatter: NoteFrontMatter;
|
frontMatter: NoteFrontMatter;
|
||||||
content: string;
|
content: string;
|
||||||
}> => {
|
}> => {
|
||||||
const fullPath = path.join(NOTES_DIR, `${slug}.mdx`);
|
const fullPath = path.join(process.cwd(), NOTES_DIR, `${slug}.mdx`);
|
||||||
const rawContent = await fs.readFile(fullPath, "utf8");
|
const rawContent = await fs.readFile(fullPath, "utf8");
|
||||||
const { data, content } = matter(rawContent);
|
const { data, content } = matter(rawContent);
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ export const getNoteData = async (
|
|||||||
smartypants: true,
|
smartypants: true,
|
||||||
}),
|
}),
|
||||||
slug,
|
slug,
|
||||||
permalink: `${baseUrl}/notes/${slug}/`,
|
permalink: `${baseUrl}/${NOTES_DIR}/${slug}/`,
|
||||||
date: formatDate(data.date), // validate/normalize the date string provided from front matter
|
date: formatDate(data.date), // validate/normalize the date string provided from front matter
|
||||||
},
|
},
|
||||||
content,
|
content,
|
||||||
|
@ -29,8 +29,8 @@
|
|||||||
"@primer/octicons": "^17.3.0",
|
"@primer/octicons": "^17.3.0",
|
||||||
"@prisma/client": "^4.0.0",
|
"@prisma/client": "^4.0.0",
|
||||||
"@react-spring/web": "^9.4.5",
|
"@react-spring/web": "^9.4.5",
|
||||||
"@sentry/node": "^7.5.0",
|
"@sentry/node": "^7.5.1",
|
||||||
"@sentry/tracing": "^7.5.0",
|
"@sentry/tracing": "^7.5.1",
|
||||||
"@stitches/react": "^1.2.8",
|
"@stitches/react": "^1.2.8",
|
||||||
"comma-number": "^2.1.0",
|
"comma-number": "^2.1.0",
|
||||||
"copy-to-clipboard": "^3.3.1",
|
"copy-to-clipboard": "^3.3.1",
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { prisma } from "../../lib/helpers/prisma";
|
import { prisma } from "../../lib/helpers/prisma";
|
||||||
import { getAllNotes } from "../../lib/helpers/parse-notes";
|
import { getAllNotes } from "../../lib/helpers/parse-notes";
|
||||||
import { logServerError } from "../../lib/helpers/sentry";
|
import { logServerError } from "../../lib/helpers/sentry";
|
||||||
|
import { NOTES_DIR } from "../../lib/config/constants";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import type { PageStats, DetailedPageStats, SiteStats } from "../../types";
|
import type { PageStats, DetailedPageStats, SiteStats } from "../../types";
|
||||||
|
|
||||||
@ -58,7 +59,8 @@ const incrementPageHits = async (slug: string): Promise<PageStats> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getSiteStats = async (): Promise<SiteStats> => {
|
const getSiteStats = async (): Promise<SiteStats> => {
|
||||||
const [pages, notes] = await Promise.all([
|
// simultaneously fetch the entire hits db and notes from the filesystem
|
||||||
|
const [hits, notes] = await Promise.all([
|
||||||
prisma.hits.findMany({
|
prisma.hits.findMany({
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{
|
{
|
||||||
@ -69,22 +71,29 @@ const getSiteStats = async (): Promise<SiteStats> => {
|
|||||||
getAllNotes(),
|
getAllNotes(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const pages: DetailedPageStats[] = [];
|
||||||
const total = { hits: 0 };
|
const total = { hits: 0 };
|
||||||
|
|
||||||
pages.forEach((page: DetailedPageStats) => {
|
hits.forEach((record) => {
|
||||||
// match URLs from RSS feed with db to populate some metadata
|
// match slugs from getAllNotes() with db results to populate some metadata
|
||||||
const match = notes.find((note) => `notes/${note.slug}` === page.slug);
|
// TODO: add support for pages other than notes.
|
||||||
|
const match = notes.find((note) => `${NOTES_DIR}/${note.slug}` === record.slug);
|
||||||
|
|
||||||
if (match) {
|
// don't reveal via API if the db entry doesn't belong to a valid page
|
||||||
page.title = match.title;
|
if (!match) {
|
||||||
page.url = match.permalink;
|
return;
|
||||||
page.date = match.date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// add these hits to running tally
|
// merge record with its matching front matter data
|
||||||
total.hits += page.hits;
|
pages.push({
|
||||||
|
...record,
|
||||||
|
title: match.title,
|
||||||
|
url: match.permalink,
|
||||||
|
date: match.date,
|
||||||
|
});
|
||||||
|
|
||||||
return page;
|
// add these hits to running tally
|
||||||
|
total.hits += record.hits;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { total, pages };
|
return { total, pages };
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { SitemapStream, SitemapItemLoose, EnumChangefreq } from "sitemap";
|
import { SitemapStream, SitemapItemLoose, EnumChangefreq } from "sitemap";
|
||||||
import { getAllNotes } from "../lib/helpers/parse-notes";
|
import { getAllNotes } from "../lib/helpers/parse-notes";
|
||||||
import { baseUrl } from "../lib/config";
|
import { baseUrl } from "../lib/config";
|
||||||
import { RELEASE_DATE } from "../lib/config/constants";
|
import { RELEASE_DATE, NOTES_DIR } from "../lib/config/constants";
|
||||||
import type { GetServerSideProps } from "next";
|
import type { GetServerSideProps } from "next";
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<Record<string, never>> = async (context) => {
|
export const getServerSideProps: GetServerSideProps<Record<string, never>> = async (context) => {
|
||||||
@ -42,16 +42,16 @@ export const getServerSideProps: GetServerSideProps<Record<string, never>> = asy
|
|||||||
const notes = await getAllNotes();
|
const notes = await getAllNotes();
|
||||||
notes.forEach((note) => {
|
notes.forEach((note) => {
|
||||||
pages.push({
|
pages.push({
|
||||||
url: `/notes/${note.slug}/`,
|
url: `/${NOTES_DIR}/${note.slug}/`,
|
||||||
// pull lastMod from front matter date
|
// pull lastMod from front matter date
|
||||||
lastmod: new Date(note.date).toISOString(),
|
lastmod: note.date,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// set lastmod of /notes/ page to most recent post's date
|
// set lastmod of /notes/ page to most recent post's date
|
||||||
pages.push({
|
pages.push({
|
||||||
url: "/notes/",
|
url: `/${NOTES_DIR}/`,
|
||||||
lastmod: new Date(notes[0].date).toISOString(),
|
lastmod: notes[0].date,
|
||||||
});
|
});
|
||||||
|
|
||||||
// sort alphabetically by URL
|
// sort alphabetically by URL
|
||||||
|
12
types/stats.d.ts
vendored
12
types/stats.d.ts
vendored
@ -1,13 +1,13 @@
|
|||||||
|
import type { NoteFrontMatter } from "./note";
|
||||||
|
|
||||||
export type PageStats = {
|
export type PageStats = {
|
||||||
hits: number;
|
hits: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DetailedPageStats = PageStats & {
|
export type DetailedPageStats = PageStats &
|
||||||
slug: string;
|
Pick<NoteFrontMatter, "slug" | "title" | "date"> & {
|
||||||
title?: string;
|
url: string;
|
||||||
url?: string;
|
};
|
||||||
date?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SiteStats = {
|
export type SiteStats = {
|
||||||
total: PageStats;
|
total: PageStats;
|
||||||
|
80
yarn.lock
80
yarn.lock
@ -1378,60 +1378,60 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.4.tgz#0c8b74c50f29ee44f423f7416829c0bf8bb5eb27"
|
resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.1.4.tgz#0c8b74c50f29ee44f423f7416829c0bf8bb5eb27"
|
||||||
integrity sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA==
|
integrity sha512-LwzQKA4vzIct1zNZzBmRKI9QuNpLgTQMEjsQLf3BXuGYb3QPTP4Yjf6mkdX+X1mYttZ808QpOwAzZjv28kq7DA==
|
||||||
|
|
||||||
"@sentry/core@7.5.0":
|
"@sentry/core@7.5.1":
|
||||||
version "7.5.0"
|
version "7.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.5.0.tgz#4ccc2312017fc6158cc379f5828dc6bbe2cdf1f7"
|
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.5.1.tgz#6ad186e671d0398dfa4552a2e5686bff6c1938d3"
|
||||||
integrity sha512-2KO2hVUki3WgvPlB0qj9+yea56CmsK2b1XtBSyAnqbs+JiXWgerF4qshVsH52kS/1h2B0CisyeIv64/WfuGvQQ==
|
integrity sha512-1ac5eaJi9LBIpCaert+IrttyaL8rnrK5fcdB6tyqDf8jNV5s9O32PyqjvjpWCrGOvZ4kmp+6UXB9bw/NNtvpkQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/hub" "7.5.0"
|
"@sentry/hub" "7.5.1"
|
||||||
"@sentry/types" "7.5.0"
|
"@sentry/types" "7.5.1"
|
||||||
"@sentry/utils" "7.5.0"
|
"@sentry/utils" "7.5.1"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/hub@7.5.0":
|
"@sentry/hub@7.5.1":
|
||||||
version "7.5.0"
|
version "7.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.5.0.tgz#30801accb9475cc3f155802a3fefd218d66fbfda"
|
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.5.1.tgz#15817d30aec9e4c1b1bbf7ded6735813f8613e66"
|
||||||
integrity sha512-R3jGEOtRtZaYCswSNs/7SmjOj/Pp8BhRyXk4q0a5GXghbuVAdzZvlJH0XnD/6jOJAF0iSXFuyGSLqVUmjkY9Ow==
|
integrity sha512-q14zzf5GlE4xvwFP7lZaAI4UnuqWMc3nD62Md5XBptY35bm42CGzawx9aDQ8cegZoQ5bHyX1GPzFju4lDO3O6g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/types" "7.5.0"
|
"@sentry/types" "7.5.1"
|
||||||
"@sentry/utils" "7.5.0"
|
"@sentry/utils" "7.5.1"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/node@^7.5.0":
|
"@sentry/node@^7.5.1":
|
||||||
version "7.5.0"
|
version "7.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.5.0.tgz#e22b27cfb19157aea5019daf6241615887e64321"
|
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.5.1.tgz#b0c94ed4bc24891c51809e75199e6d1f8de284f2"
|
||||||
integrity sha512-lcHIgzcOjKnBeXf9CPbHOwFTuRf+huYMwhF3IVz5Ewbpm4eZn3LPk838ypqtspD7UtFbAdaTGXs/w3Y9P3Zi4g==
|
integrity sha512-XSpNbxBVIpcklLpk9NtQSkTZM0/mEj0TYMnzQmE2UR7UChpGhZyc19nbQWceSsaLMrrAgOX4Zzo28ENk/QY5FA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/core" "7.5.0"
|
"@sentry/core" "7.5.1"
|
||||||
"@sentry/hub" "7.5.0"
|
"@sentry/hub" "7.5.1"
|
||||||
"@sentry/types" "7.5.0"
|
"@sentry/types" "7.5.1"
|
||||||
"@sentry/utils" "7.5.0"
|
"@sentry/utils" "7.5.1"
|
||||||
cookie "^0.4.1"
|
cookie "^0.4.1"
|
||||||
https-proxy-agent "^5.0.0"
|
https-proxy-agent "^5.0.0"
|
||||||
lru_map "^0.3.3"
|
lru_map "^0.3.3"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/tracing@^7.5.0":
|
"@sentry/tracing@^7.5.1":
|
||||||
version "7.5.0"
|
version "7.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.5.0.tgz#ad6da27563246e9d754c36a2a3d398cfb979117e"
|
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.5.1.tgz#86aca47db8de2c940ca587fb771acd2a88bff815"
|
||||||
integrity sha512-tSVnCJNImsWms4tBhJ2Xr+HI1i9zKg4eZ0dImi93/H3sf5hmK9r2E11Xs/8rTxqpGWzB8axVi2tcmqmfqXKGTg==
|
integrity sha512-fOmzTk3/mTKF5d1P43yZ29lc5z/1wyL2+qX+N5rLluIeR6dEYISyjFistK8z1esMCCvJu8/x3u0imbrrFDNx0Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/hub" "7.5.0"
|
"@sentry/hub" "7.5.1"
|
||||||
"@sentry/types" "7.5.0"
|
"@sentry/types" "7.5.1"
|
||||||
"@sentry/utils" "7.5.0"
|
"@sentry/utils" "7.5.1"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@sentry/types@7.5.0":
|
"@sentry/types@7.5.1":
|
||||||
version "7.5.0"
|
version "7.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.5.0.tgz#610f14c1219ba461ca84a3c89e06de8c0cf357bc"
|
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.5.1.tgz#3dd647973fee256588f92d3d74d8ce560b73758b"
|
||||||
integrity sha512-VPQ/53mLo5N8NQUB4k6R2GQBWoW8otFyhhPnC75gYXeBTItVCzJAylVyWy8b+gGqGst+pQN3wb2dl9xhrd69YQ==
|
integrity sha512-+OHxQL4lXCEsUA31qlhcPABOjxtbuL+VTpgamXJjxEpQQDPUPyPK0pu7c+uTc7x4Re96Ss3pwUYE9tl3WW3xIg==
|
||||||
|
|
||||||
"@sentry/utils@7.5.0":
|
"@sentry/utils@7.5.1":
|
||||||
version "7.5.0"
|
version "7.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.5.0.tgz#64435ea094aa7d79d1dfe7586d2d5a2bff9e3839"
|
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.5.1.tgz#10877a19372040ebf5bc0342fed69b8147a8d269"
|
||||||
integrity sha512-DgHrkGgHplVMgMbU9hGBfGBV6LcOwNBrhHiVaFwo2NHiXnGwMkaILi5XTRjKm9Iu/m2choAFABA80HEtPKmjtA==
|
integrity sha512-5w5dEDilAkH/4x5h8VMlfFcGKdDQ8tbSEfxnMOheD3/bwk18lTVTgp6kk+VxmugGdvxsTLiPEoORsuofufWvGQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/types" "7.5.0"
|
"@sentry/types" "7.5.1"
|
||||||
tslib "^1.9.3"
|
tslib "^1.9.3"
|
||||||
|
|
||||||
"@stitches/react@^1.2.8":
|
"@stitches/react@^1.2.8":
|
||||||
@ -1653,9 +1653,9 @@
|
|||||||
"@types/unist" "*"
|
"@types/unist" "*"
|
||||||
|
|
||||||
"@types/node@*":
|
"@types/node@*":
|
||||||
version "18.0.2"
|
version "18.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.2.tgz#a594e580c396c22dd6b1470be81737c79ec0b1b1"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.3.tgz#463fc47f13ec0688a33aec75d078a0541a447199"
|
||||||
integrity sha512-b947SdS4GH+g2W33wf5FzUu1KLj5FcSIiNWbU1ZyMvt/X7w48ZsVcsQoirIgE/Oq03WT5Qbn/dkY0hePi4ZXcQ==
|
integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ==
|
||||||
|
|
||||||
"@types/node@^17.0.5":
|
"@types/node@^17.0.5":
|
||||||
version "17.0.45"
|
version "17.0.45"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user