mirror of
https://github.com/vercel/commerce.git
synced 2025-06-20 06:01:21 +00:00
feat(recipes): generate recipe pages from airtable (#178995849)
This commit is contained in:
parent
e4a45a7ea3
commit
ff7a5a3728
51
lib/api/airtable.ts
Normal file
51
lib/api/airtable.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import Airtable from 'airtable'
|
||||||
|
|
||||||
|
const API_KEY = process.env.AIRTABLE_API_KEY || ''
|
||||||
|
const BASE_ID = process.env.AIRTABLE_BASE_ID || ''
|
||||||
|
|
||||||
|
const base = new Airtable({ apiKey: API_KEY }).base(BASE_ID)
|
||||||
|
|
||||||
|
export interface Recipe {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
slug: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRecipePages = async (): Promise<string[]> =>
|
||||||
|
new Promise((resolve, reject) =>
|
||||||
|
base('Recipes')
|
||||||
|
.select({
|
||||||
|
view: 'Pages',
|
||||||
|
filterByFormula: "{Status} = 'Live'",
|
||||||
|
fields: ['Slug'],
|
||||||
|
})
|
||||||
|
.all((error, records = []) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
} else {
|
||||||
|
resolve(records.map((recipe) => recipe.get('Slug') as string))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export const getRecipes = async (): Promise<Recipe[]> =>
|
||||||
|
new Promise((resolve, reject) =>
|
||||||
|
base('Recipes')
|
||||||
|
.select({
|
||||||
|
view: 'Pages',
|
||||||
|
filterByFormula: "{Status} = 'Live'",
|
||||||
|
})
|
||||||
|
.all((error, records = []) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
} else {
|
||||||
|
resolve(
|
||||||
|
records.map((recipe) => ({
|
||||||
|
id: recipe.getId(),
|
||||||
|
title: recipe.get('Recipes') as string,
|
||||||
|
slug: recipe.get('Slug') as string,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
@ -14,7 +14,7 @@ const isVendure = provider === 'vendure'
|
|||||||
module.exports = withCommerceConfig({
|
module.exports = withCommerceConfig({
|
||||||
commerce,
|
commerce,
|
||||||
i18n: {
|
i18n: {
|
||||||
locales: ['en-US', 'es'],
|
locales: ['en-US'],
|
||||||
defaultLocale: 'en-US',
|
defaultLocale: 'en-US',
|
||||||
},
|
},
|
||||||
rewrites() {
|
rewrites() {
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
"dev": "NODE_OPTIONS='--inspect' next dev",
|
"dev": "NODE_OPTIONS='--inspect' next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
"analyze": "BUNDLE_ANALYZE=both yarn build",
|
"analyze": "BUNDLE_ANALYZE=both yarn build",
|
||||||
"prettier-fix": "prettier --write .",
|
"prettier-fix": "prettier --write .",
|
||||||
"find:unused": "npx next-unused",
|
"find:unused": "npx next-unused",
|
||||||
@ -21,6 +22,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-spring/web": "^9.2.1",
|
"@react-spring/web": "^9.2.1",
|
||||||
"@vercel/fetch": "^6.1.0",
|
"@vercel/fetch": "^6.1.0",
|
||||||
|
"airtable": "^0.11.1",
|
||||||
"autoprefixer": "^10.2.6",
|
"autoprefixer": "^10.2.6",
|
||||||
"body-scroll-lock": "^3.1.5",
|
"body-scroll-lock": "^3.1.5",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
@ -1,86 +0,0 @@
|
|||||||
import type {
|
|
||||||
GetStaticPathsContext,
|
|
||||||
GetStaticPropsContext,
|
|
||||||
InferGetStaticPropsType,
|
|
||||||
} from 'next'
|
|
||||||
import commerce from '@lib/api/commerce'
|
|
||||||
import { Text } from '@components/ui'
|
|
||||||
import { Layout } from '@components/common'
|
|
||||||
import getSlug from '@lib/get-slug'
|
|
||||||
import { missingLocaleInPages } from '@lib/usage-warns'
|
|
||||||
import type { Page } from '@commerce/types/page'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
|
|
||||||
export async function getStaticProps({
|
|
||||||
preview,
|
|
||||||
params,
|
|
||||||
locale,
|
|
||||||
locales,
|
|
||||||
}: GetStaticPropsContext<{ pages: string[] }>) {
|
|
||||||
const config = { locale, locales }
|
|
||||||
const pagesPromise = commerce.getAllPages({ config, preview })
|
|
||||||
const siteInfoPromise = commerce.getSiteInfo({ config, preview })
|
|
||||||
const { pages } = await pagesPromise
|
|
||||||
const { categories } = await siteInfoPromise
|
|
||||||
const path = params?.pages.join('/')
|
|
||||||
const slug = locale ? `${locale}/${path}` : path
|
|
||||||
const pageItem = pages.find((p: Page) =>
|
|
||||||
p.url ? getSlug(p.url) === slug : false
|
|
||||||
)
|
|
||||||
const data =
|
|
||||||
pageItem &&
|
|
||||||
(await commerce.getPage({
|
|
||||||
variables: { id: pageItem.id! },
|
|
||||||
config,
|
|
||||||
preview,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const page = data?.page
|
|
||||||
|
|
||||||
if (!page) {
|
|
||||||
// We throw to make sure this fails at build time as this is never expected to happen
|
|
||||||
throw new Error(`Page with slug '${slug}' not found`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: { pages, page, categories },
|
|
||||||
revalidate: 60 * 60, // Every hour
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStaticPaths({ locales }: GetStaticPathsContext) {
|
|
||||||
const config = { locales }
|
|
||||||
const { pages }: { pages: Page[] } = await commerce.getAllPages({ config })
|
|
||||||
const [invalidPaths, log] = missingLocaleInPages()
|
|
||||||
const paths = pages
|
|
||||||
.map((page) => page.url)
|
|
||||||
.filter((url) => {
|
|
||||||
if (!url || !locales) return url
|
|
||||||
// If there are locales, only include the pages that include one of the available locales
|
|
||||||
if (locales.includes(getSlug(url).split('/')[0])) return url
|
|
||||||
|
|
||||||
invalidPaths.push(url)
|
|
||||||
})
|
|
||||||
log()
|
|
||||||
|
|
||||||
return {
|
|
||||||
paths,
|
|
||||||
fallback: 'blocking',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Pages({
|
|
||||||
page,
|
|
||||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
return router.isFallback ? (
|
|
||||||
<h1>Loading...</h1> // TODO (BC) Add Skeleton Views
|
|
||||||
) : (
|
|
||||||
<div className="max-w-2xl mx-8 sm:mx-auto py-20">
|
|
||||||
{page?.body && <Text html={page.body} />}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Pages.Layout = Layout
|
|
@ -1,81 +0,0 @@
|
|||||||
import type {
|
|
||||||
GetStaticPathsContext,
|
|
||||||
GetStaticPropsContext,
|
|
||||||
InferGetStaticPropsType,
|
|
||||||
} from 'next'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
import commerce from '@lib/api/commerce'
|
|
||||||
import { Layout } from '@components/common'
|
|
||||||
import { ProductView } from '@components/product'
|
|
||||||
|
|
||||||
export async function getStaticProps({
|
|
||||||
params,
|
|
||||||
locale,
|
|
||||||
locales,
|
|
||||||
preview,
|
|
||||||
}: GetStaticPropsContext<{ slug: string }>) {
|
|
||||||
const config = { locale, locales }
|
|
||||||
const pagesPromise = commerce.getAllPages({ config, preview })
|
|
||||||
const siteInfoPromise = commerce.getSiteInfo({ config, preview })
|
|
||||||
const productPromise = commerce.getProduct({
|
|
||||||
variables: { slug: params!.slug },
|
|
||||||
config,
|
|
||||||
preview,
|
|
||||||
})
|
|
||||||
|
|
||||||
const allProductsPromise = commerce.getAllProducts({
|
|
||||||
variables: { first: 4 },
|
|
||||||
config,
|
|
||||||
preview,
|
|
||||||
})
|
|
||||||
const { pages } = await pagesPromise
|
|
||||||
const { categories } = await siteInfoPromise
|
|
||||||
const { product } = await productPromise
|
|
||||||
const { products: relatedProducts } = await allProductsPromise
|
|
||||||
|
|
||||||
if (!product) {
|
|
||||||
throw new Error(`Product with slug '${params!.slug}' not found`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
pages,
|
|
||||||
product,
|
|
||||||
relatedProducts,
|
|
||||||
categories,
|
|
||||||
},
|
|
||||||
revalidate: 200,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStaticPaths({ locales }: GetStaticPathsContext) {
|
|
||||||
const { products } = await commerce.getAllProductPaths()
|
|
||||||
|
|
||||||
return {
|
|
||||||
paths: locales
|
|
||||||
? locales.reduce<string[]>((arr, locale) => {
|
|
||||||
// Add a product path for every locale
|
|
||||||
products.forEach((product: any) => {
|
|
||||||
arr.push(`/${locale}/product${product.path}`)
|
|
||||||
})
|
|
||||||
return arr
|
|
||||||
}, [])
|
|
||||||
: products.map((product: any) => `/product${product.path}`),
|
|
||||||
fallback: 'blocking',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Slug({
|
|
||||||
product,
|
|
||||||
relatedProducts,
|
|
||||||
}: InferGetStaticPropsType<typeof getStaticProps>) {
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
return router.isFallback ? (
|
|
||||||
<h1>Loading...</h1>
|
|
||||||
) : (
|
|
||||||
<ProductView product={product} relatedProducts={relatedProducts} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Slug.Layout = Layout
|
|
64
pages/recipes/[slug].tsx
Normal file
64
pages/recipes/[slug].tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next'
|
||||||
|
|
||||||
|
import { getRecipePages, getRecipes } from '@lib/api/airtable'
|
||||||
|
import { Text } from '@components/ui'
|
||||||
|
|
||||||
|
interface Recipe {
|
||||||
|
slug: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getStaticProps = async ({
|
||||||
|
params,
|
||||||
|
}: GetStaticPropsContext<{ slug: string }>) => {
|
||||||
|
// const config = { locale, locales }
|
||||||
|
// const pagesPromise = commerce.getAllPages({ config, preview })
|
||||||
|
// const siteInfoPromise = commerce.getSiteInfo({ config, preview })
|
||||||
|
// const { pages } = await pagesPromise
|
||||||
|
// const { categories } = await siteInfoPromise
|
||||||
|
// const path = params?.pages.join('/')
|
||||||
|
const recipes = await getRecipes()
|
||||||
|
const recipe = recipes.find((recipe) => recipe.slug === params?.slug)
|
||||||
|
// const pageItem = pages.find((p: Page) =>
|
||||||
|
// p.url ? getSlug(p.url) === slug : false
|
||||||
|
// )
|
||||||
|
// const data =
|
||||||
|
// pageItem &&
|
||||||
|
// (await commerce.getPage({
|
||||||
|
// variables: { id: pageItem.id! },
|
||||||
|
// config,
|
||||||
|
// preview,
|
||||||
|
// }))
|
||||||
|
|
||||||
|
// const page = data?.page
|
||||||
|
|
||||||
|
if (!recipe) {
|
||||||
|
// We throw to make sure this fails at build time as this is never expected to happen
|
||||||
|
throw new Error(`Page with slug '${params?.slug}' not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: { recipe },
|
||||||
|
revalidate: 60 * 60, // Every hour
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getStaticPaths = async () => {
|
||||||
|
const pages = await getRecipePages()
|
||||||
|
return {
|
||||||
|
paths: pages.map((slug) => ({
|
||||||
|
params: {
|
||||||
|
slug,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
fallback: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Recipe = ({ recipe }: InferGetStaticPropsType<typeof getStaticProps>) => (
|
||||||
|
<div className="max-w-2xl mx-8 sm:mx-auto py-20">
|
||||||
|
<Text>{recipe.title}</Text>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Recipe
|
@ -34,6 +34,11 @@
|
|||||||
"./framework/shopify",
|
"./framework/shopify",
|
||||||
"./framework/swell",
|
"./framework/swell",
|
||||||
"./framework/vendure",
|
"./framework/vendure",
|
||||||
"./framework/saleor"
|
"./framework/saleor",
|
||||||
|
"framework/bigcommerce",
|
||||||
|
"framework/saleor",
|
||||||
|
"framework/swell",
|
||||||
|
"framework/vendure",
|
||||||
|
"framework/local"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
41
yarn.lock
41
yarn.lock
@ -1116,6 +1116,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
|
||||||
integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==
|
integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==
|
||||||
|
|
||||||
|
"@types/node@>=8.0.0 <15":
|
||||||
|
version "14.17.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.5.tgz#b59daf6a7ffa461b5648456ca59050ba8e40ed54"
|
||||||
|
integrity sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==
|
||||||
|
|
||||||
"@types/parse-json@^4.0.0":
|
"@types/parse-json@^4.0.0":
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
@ -1140,6 +1145,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
|
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275"
|
||||||
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
|
integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA==
|
||||||
|
|
||||||
|
"@types/uuid@8.3.1":
|
||||||
|
version "8.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.1.tgz#1a32969cf8f0364b3d8c8af9cc3555b7805df14f"
|
||||||
|
integrity sha512-Y2mHTRAbqfFkpjldbkHGY8JIzRN6XqYRliG8/24FcHm2D2PwW24fl5xMRTVGdrb7iMrwCaIEbLWerGIkXuFWVg==
|
||||||
|
|
||||||
"@types/websocket@1.0.2":
|
"@types/websocket@1.0.2":
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.2.tgz#d2855c6a312b7da73ed16ba6781815bf30c6187a"
|
resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.2.tgz#d2855c6a312b7da73ed16ba6781815bf30c6187a"
|
||||||
@ -1185,13 +1195,18 @@
|
|||||||
async-retry "1.2.3"
|
async-retry "1.2.3"
|
||||||
lru-cache "5.1.1"
|
lru-cache "5.1.1"
|
||||||
|
|
||||||
abort-controller@3.0.0:
|
abort-controller@3.0.0, abort-controller@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
|
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
|
||||||
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
|
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
|
||||||
dependencies:
|
dependencies:
|
||||||
event-target-shim "^5.0.0"
|
event-target-shim "^5.0.0"
|
||||||
|
|
||||||
|
abortcontroller-polyfill@^1.4.0:
|
||||||
|
version "1.7.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz#1b5b487bd6436b5b764fd52a612509702c3144b5"
|
||||||
|
integrity sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q==
|
||||||
|
|
||||||
acorn-node@^1.6.1:
|
acorn-node@^1.6.1:
|
||||||
version "1.8.2"
|
version "1.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
|
resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8"
|
||||||
@ -1243,6 +1258,17 @@ aggregate-error@^3.0.0:
|
|||||||
clean-stack "^2.0.0"
|
clean-stack "^2.0.0"
|
||||||
indent-string "^4.0.0"
|
indent-string "^4.0.0"
|
||||||
|
|
||||||
|
airtable@^0.11.1:
|
||||||
|
version "0.11.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/airtable/-/airtable-0.11.1.tgz#2fda51da04f5e4be7092351ea7be3cfdcf308347"
|
||||||
|
integrity sha512-33zBuUDhLl+FWWAFxFjS1a+vJr/b+UK//EV943nuiimChWph6YykQjYPmu/GucQ30g7mgaqq+98uPD4rfDHOgg==
|
||||||
|
dependencies:
|
||||||
|
"@types/node" ">=8.0.0 <15"
|
||||||
|
abort-controller "^3.0.0"
|
||||||
|
abortcontroller-polyfill "^1.4.0"
|
||||||
|
lodash "^4.17.21"
|
||||||
|
node-fetch "^2.6.1"
|
||||||
|
|
||||||
anser@1.4.9:
|
anser@1.4.9:
|
||||||
version "1.4.9"
|
version "1.4.9"
|
||||||
resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.9.tgz#1f85423a5dcf8da4631a341665ff675b96845760"
|
resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.9.tgz#1f85423a5dcf8da4631a341665ff675b96845760"
|
||||||
@ -6044,6 +6070,19 @@ util@^0.12.0:
|
|||||||
safe-buffer "^5.1.2"
|
safe-buffer "^5.1.2"
|
||||||
which-typed-array "^1.1.2"
|
which-typed-array "^1.1.2"
|
||||||
|
|
||||||
|
uuid@8.3.2:
|
||||||
|
version "8.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
|
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||||
|
|
||||||
|
uuidv4@^6.2.10:
|
||||||
|
version "6.2.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuidv4/-/uuidv4-6.2.11.tgz#34d5a03324eb38296b87ae523a64233b5286cc27"
|
||||||
|
integrity sha512-OTS4waH9KplrXNADKo+Q1kT9AHWr8DaC0S5F54RQzEwcUaEzBEWQQlJyDUw/u1bkRhJyqkqhLD4M4lbFbV+89g==
|
||||||
|
dependencies:
|
||||||
|
"@types/uuid" "8.3.1"
|
||||||
|
uuid "8.3.2"
|
||||||
|
|
||||||
valid-url@1.0.9, valid-url@^1.0.9:
|
valid-url@1.0.9, valid-url@^1.0.9:
|
||||||
version "1.0.9"
|
version "1.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"
|
resolved "https://registry.yarnpkg.com/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user