chore: initializes standalone repository

This commit is contained in:
Jacob Fletcher
2022-02-20 12:39:58 -05:00
parent 6d76091fa1
commit 2ba6bf69c1
25 changed files with 11081 additions and 230 deletions

View File

@@ -1,3 +1,4 @@
node_modules
.env
dist
demo/uploads

View File

@@ -0,0 +1,106 @@
# Payload SEO Plugin
[![NPM](https://img.shields.io/npm/v/payload-plugin-seo)](https://www.npmjs.com/package/payload-plugin-seo)
A plugin for [Payload CMS](https://github.com/payloadcms/payload) to auto-generate custom meta data based on the content of your documents.
Core features:
- Adds a `meta` field to every seo-enabled collection that:
- Includes title, description, and image subfields
- Auto-generates meta data from your document's content
- Displays hints and indicators to help content editors
- Renders a snippet of what a search engine might display
- Soon: variable injection
## Installation
```bash
yarn add payload-plugin-seo
# OR
npm i payload-plugin-seo
```
## Basic Usage
In the `plugins` array of your [Payload config](https://payloadcms.com/docs/configuration/overview), call the plugin with [options](#options):
```js
import { buildConfig } from 'payload/config';
import seo from 'payload-seo';
const config = buildConfig({
collections: [
{
slug: 'pages',
fields: []
},
{
slug: 'media',
upload: {
staticDir: // path to your static directory,
},
fields: []
}
],
plugins: [
seo({
collections: [
'pages',
],
uploadsCollection: 'media',
generateTitle: ({ doc }) => `Website.com — ${doc.title}`,
generateDescription: ({ doc }) => doc.excerpt
})
]
});
export default config;
```
### Options
- `collections`
An array of collections slugs to enable SEO. Enabled collections receive a `meta` field which is an object of title, description, and image subfields.
- `uploadsCollection`
An upload-enabled collection slug, for the meta image to access.
- `generateTitle`
Lorem
```js
seo({
...
generateTitle: ({ doc }) => `Website.com — ${doc.title}`,
})
```
- `generateDescription`
Lorem
```js
seo({
...
generateDescription: ({ doc }) => doc.excerpt
})
## TypeScript
All types can be directly imported:
```js
import {
SEOConfig,
GenerateTitle,
GenerateDescription
} from 'payload-plugin-seo/dist/types';
```
## Screenshots
<!-- ![screenshot 1](https://github.com/trouble/payload-plugin-seo/blob/main/images/screenshot-1.jpg?raw=true) -->

View File

@@ -0,0 +1,4 @@
{
"ext": "ts",
"exec": "ts-node src/server.ts"
}

View File

@@ -0,0 +1,27 @@
{
"name": "payload-starter-typescript",
"description": "Blank template - no collections",
"version": "1.0.0",
"main": "dist/server.js",
"license": "MIT",
"scripts": {
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
"build:server": "tsc",
"build": "yarn build:payload && yarn build:server",
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types"
},
"dependencies": {
"payload": "^0.14.24-beta.0",
"dotenv": "^8.2.0",
"express": "^4.17.1"
},
"devDependencies": {
"@types/express": "^4.17.9",
"cross-env": "^7.0.3",
"nodemon": "^2.0.6",
"ts-node": "^9.1.1",
"typescript": "^4.1.3"
}
}

View File

@@ -0,0 +1,25 @@
import { CollectionConfig } from 'payload/types';
import path from 'path';
const Media: CollectionConfig = {
slug: 'media',
access: {
read: (): boolean => true,
},
admin: {
useAsTitle: 'filename',
},
upload: {
staticDir: path.resolve(__dirname, '../../uploads'),
},
fields: [
{
name: 'alt',
label: 'Alt Text',
type: 'text',
required: true,
},
]
}
export default Media;

View File

@@ -0,0 +1,20 @@
import { CollectionConfig } from 'payload/types';
const Pages: CollectionConfig = {
slug: 'pages',
admin: {
useAsTitle: 'title',
},
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'excerpt',
type: 'text',
},
],
}
export default Pages;

View File

@@ -0,0 +1,18 @@
import { CollectionConfig } from 'payload/types';
const Users: CollectionConfig = {
slug: 'users',
auth: true,
admin: {
useAsTitle: 'email',
},
access: {
read: () => true,
},
fields: [
// Email added by default
// Add more fields as needed
],
};
export default Users;

View File

@@ -0,0 +1,48 @@
import { buildConfig } from 'payload/config';
import path from 'path';
import seo from '../../dist';
// import seo from '../../src';
import Users from './collections/Users';
import Pages from './collections/Pages';
import Media from './collections/Media';
export default buildConfig({
serverURL: 'http://localhost:3000',
admin: {
user: Users.slug,
webpack: (config) => {
const newConfig = {
...config,
resolve: {
...config.resolve,
alias: {
...config.resolve.alias,
react: path.join(__dirname, "../node_modules/react"),
"react-dom": path.join(__dirname, "../node_modules/react-dom"),
"payload": path.join(__dirname, "../node_modules/payload"),
},
},
};
return newConfig;
},
},
collections: [
Users,
Pages,
Media
],
plugins: [
seo({
collections: [
'pages',
],
uploadsCollection: 'media',
generateTitle: ({ doc }) => `Website.com — ${doc.title}`,
generateDescription: ({ doc }) => doc.excerpt
}),
],
typescript: {
outputFile: path.resolve(__dirname, 'payload-types.ts')
},
});

View File

@@ -0,0 +1,24 @@
import express from 'express';
import payload from 'payload';
require('dotenv').config();
const app = express();
// Redirect root to Admin panel
app.get('/', (_, res) => {
res.redirect('/admin');
});
// Initialize Payload
payload.init({
secret: process.env.PAYLOAD_SECRET,
mongoURL: process.env.MONGODB_URI,
express: app,
onInit: () => {
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
},
});
// Add your own express routes here
app.listen(3000);

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react",
},
"ts-node": {
"transpileOnly": true
}
}

View File

@@ -0,0 +1,57 @@
Arguments:
/Users/jacobfletcher/.nvm/versions/node/v14.18.0/bin/node /Users/jacobfletcher/.nvm/versions/node/v14.18.0/bin/yarn
PATH:
/Users/jacobfletcher/.nvm/versions/node/v14.18.0/bin:/Users/jacobfletcher/.yarn/bin:/Users/jacobfletcher/.config/yarn/global/node_modules/.bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/jacobfletcher/.nvm/versions/node/v14.18.0/bin:/Users/jacobfletcher/.yarn/bin:/Users/jacobfletcher/.config/yarn/global/node_modules/.bin
Yarn version:
1.22.17
Node version:
14.18.0
Platform:
darwin x64
Trace:
SyntaxError: /Users/jacobfletcher/dev/trbl/payload-plugin-seo/package.json: Unexpected token } in JSON at position 652
at JSON.parse (<anonymous>)
at /Users/jacobfletcher/.nvm/versions/node/v14.18.0/lib/node_modules/yarn/lib/cli.js:1625:59
at Generator.next (<anonymous>)
at step (/Users/jacobfletcher/.nvm/versions/node/v14.18.0/lib/node_modules/yarn/lib/cli.js:310:30)
at /Users/jacobfletcher/.nvm/versions/node/v14.18.0/lib/node_modules/yarn/lib/cli.js:321:13
npm manifest:
{
"name": "payload-starter-typescript",
"description": "Blank template - no collections",
"version": "1.0.0",
"main": "dist/server.js",
"license": "MIT",
"scripts": {
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
"build:server": "tsc",
"build": "yarn build:payload && yarn build:server",
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types"
},
"dependencies": {
"payload": "^0.14.0",
"dotenv": "^8.2.0",
"express": "^4.17.1"
},
"devDependencies": {
"@types/express": "^4.17.9",
"cross-env": "^7.0.3",
"nodemon": "^2.0.6",
"ts-node": "^9.1.1",
"typescript": "^4.1.3"
}
}
yarn manifest:
No manifest
Lockfile:
No lockfile

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "payload-plugin-seo",
"version": "1.0.0",
"version": "0.0.1",
"description": "SEO plugin for Payload CMS",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -31,4 +31,7 @@
"files": [
"dist"
],
"dependencies": {
"@payloadcms/config-provider": "^1.0.0"
}
}

View File

@@ -1,12 +1,9 @@
/* eslint-disable no-use-before-define */
/* eslint-disable import/no-extraneous-dependencies */
import React, { useCallback } from 'react';
import { Props as TextFieldType } from 'payload/dist/admin/components/forms/field-types/Text/types';
import { useField, useWatchForm } from 'payload/components/forms';
import { FieldType, Options } from 'payload/dist/admin/components/forms/useField/types';
import { LengthIndicator } from '../ui/LengthIndicator';
import { defaults } from '../defaults';
import { generateMetaDescription } from '../utilities/generateMetaDescription';
import TextareaInput from 'payload/dist/admin/components/forms/field-types/Textarea/Input';
const {
@@ -30,9 +27,17 @@ export const MetaDescription: React.FC<TextFieldType> = (props) => {
showError
} = field;
let generateDescription: string | ((doc: any) => void);
const regenerateDescription = useCallback(() => {
const generatedDesc = generateMetaDescription(fields);
setValue(generatedDesc);
const getDescription = async () => {
let generatedDescription;
if (typeof generateDescription === 'function') {
generatedDescription = await generateDescription({ fields });
}
setValue(generatedDescription);
}
getDescription();
}, [
fields,
setValue,

View File

@@ -1,12 +1,9 @@
/* eslint-disable no-use-before-define */
/* eslint-disable import/no-extraneous-dependencies */
import React, { useCallback } from 'react';
import { useConfig } from '@payloadcms/config-provider';
import { Props as UploadFieldType } from 'payload/dist/admin/components/forms/field-types/Upload/types';
import UploadInput from 'payload/dist/admin/components/forms/field-types/Upload/Input';
import { useField, useWatchForm } from 'payload/components/forms';
import { FieldType, Options } from 'payload/dist/admin/components/forms/useField/types';
import { generateMetaImage } from '../utilities/generateMetaImage';
import { Pill } from '../ui/Pill';
export const MetaImage: React.FC<UploadFieldType> = (props) => {
@@ -27,9 +24,17 @@ export const MetaImage: React.FC<UploadFieldType> = (props) => {
showError,
} = field;
let generateImage: string | ((doc: any) => void);
const regenerateImage = useCallback(() => {
const generatedImage = generateMetaImage(fields);
const getDescription = async () => {
let generatedImage;
if (typeof generateImage === 'function') {
generatedImage = await generateImage({ fields });
}
setValue(generatedImage);
}
getDescription();
}, [
fields,
setValue,
@@ -37,7 +42,7 @@ export const MetaImage: React.FC<UploadFieldType> = (props) => {
const hasImage = Boolean(value);
const config = useConfig();
const config = useConfig(); // TODO: this returns an empty object
const {
collections,
@@ -109,7 +114,7 @@ export const MetaImage: React.FC<UploadFieldType> = (props) => {
setValue(null);
}
}}
label={null}
label={undefined}
showError={showError}
api={api}
collection={collection}

View File

@@ -1,5 +1,3 @@
/* eslint-disable no-use-before-define */
/* eslint-disable import/no-extraneous-dependencies */
import React, { useCallback } from 'react';
import { Props as TextFieldType } from 'payload/dist/admin/components/forms/field-types/Text/types';
import TextInputField from 'payload/dist/admin/components/forms/field-types/Text/Input';
@@ -7,7 +5,6 @@ import { useField, useWatchForm } from 'payload/components/forms';
import { FieldType as FieldType, Options } from 'payload/dist/admin/components/forms/useField/types';
import { LengthIndicator } from '../ui/LengthIndicator';
import { defaults } from '../defaults';
import { generateMetaTitle } from '../utilities/generateMetaTitle';
const {
minLength,
@@ -30,9 +27,14 @@ export const MetaTitle: React.FC<TextFieldType> = (props) => {
showError
} = field;
let generateTitle: string | ((doc: any) => void);
const regenerateTitle = useCallback(() => {
const getTitle = async () => {
const generatedTitle = await generateMetaTitle(fields);
let generatedTitle;
if (typeof generateTitle === 'function') {
generatedTitle = await generateTitle({ fields });
}
setValue(generatedTitle);
}
getTitle();

View File

@@ -5,22 +5,20 @@ import { Overview } from './ui/Overview';
import { MetaTitle } from './fields/MetaTitle';
import { Preview } from './ui/Preview';
import { MetaImage } from './fields/MetaImage';
import { SEOConfig } from './types';
type Options = {
collections?: string[]
}
const seo = (options: Options) => (config: Config): Config => ({
const seo = (seoConfig: SEOConfig) => (config: Config): Config => ({
...config,
collections: config.collections.map((collection) => {
collections: config.collections?.map((collection) => {
const { slug } = collection;
const addMeta = options?.collections?.includes(slug);
const isEnabled = seoConfig?.collections?.includes(slug);
if (isEnabled) {
return ({
...collection,
fields: [
...collection.fields,
...addMeta ? [{
...collection?.fields || [],
{
name: 'meta',
label: 'SEO',
type: 'group',
@@ -59,7 +57,7 @@ const seo = (options: Options) => (config: Config): Config => ({
name: 'image',
label: 'Meta Image',
type: 'upload',
relationTo: 'media',
relationTo: seoConfig?.uploadsCollection || '',
admin: {
description: 'Maximum upload file size: 12MB. Recommended file size for images is <500KB.',
components: {
@@ -78,9 +76,11 @@ const seo = (options: Options) => (config: Config): Config => ({
},
},
],
}] : [],
},
],
}) as CollectionConfig;
}
return collection;
}),
});

View File

@@ -0,0 +1,6 @@
export type SEOConfig = {
collections?: string[]
uploadsCollection?: string
generateTitle?: (args: { doc: any }) => string | Promise<string>
generateDescription?: (args: { doc: any }) => string | Promise<string>
}

View File

@@ -11,8 +11,8 @@ export const LengthIndicator: React.FC<{
}> = (props) => {
const {
text,
minLength,
maxLength,
minLength = 0,
maxLength = 0,
} = props;
const [labelStyle, setLabelStyle] = useState({

View File

@@ -61,7 +61,7 @@ export const Preview: React.FC = () => {
textDecoration: 'none',
}}
>
{metaTitle}
{metaTitle as string}
</a>
</h4>
<p
@@ -70,7 +70,7 @@ export const Preview: React.FC = () => {
color: '#202124',
}}
>
{metaDescription}
{metaDescription as string}
</p>
</div>
</div>

View File

@@ -1,65 +0,0 @@
import getDataByPath from 'payload/dist/admin/components/forms/Form/getDataByPath';
import { Fields } from 'payload/dist/admin/components/forms/Form/types';
import { stringifyRichText } from './stringifyRichText';
export const generateMetaDescription = (fields: Fields): string => {
const {
excerpt: {
value: excerpt,
},
} = fields;
let description = excerpt as string || '';
if (!excerpt) {
const firstBlock = getDataByPath(fields, 'layout.0');
if (firstBlock) {
// instead of writing a custom block for every type, we'll just iterate threw some generic keys and write a block each of those
const commonKeys = [
'introContent',
'richText',
'columns',
];
const blockKeys = Object.keys(firstBlock);
const matchingKeys = commonKeys.filter((commonKey) => blockKeys.includes(commonKey));
if (matchingKeys.length > -1) {
const keyToUse = matchingKeys[0];
const field = firstBlock?.[keyToUse];
if (field) {
if (keyToUse === 'introContent') {
const introContent = stringifyRichText(field);
if (!description && introContent) description = introContent;
}
if (keyToUse === 'richText') {
const richText = stringifyRichText(field);
if (!description && richText) description = richText;
}
if (keyToUse === 'columns') {
const firstColumn = field[0];
if (firstColumn) {
const {
richText,
} = firstColumn;
let richTextToUse = richText;
if (typeof richText === 'string') {
richTextToUse = JSON.parse(richText);
}
const columnText = stringifyRichText(richTextToUse);
if (!description && columnText) description = columnText;
}
}
}
}
}
}
return description;
};

View File

@@ -1,13 +0,0 @@
import { Fields } from 'payload/dist/admin/components/forms/Form/types';
export const generateMetaImage = (fields: Fields): string => {
let image;
const heroType = fields?.['hero.type']?.value;
if (heroType) {
image = fields?.[`hero.${heroType}.media`]?.value;
}
return image;
};

View File

@@ -1,37 +0,0 @@
import { Fields } from 'payload/dist/admin/components/forms/Form/types';
const base = 'Hope Network';
export const generateMetaTitle = async (fields: Fields): Promise<string> => {
const {
title: {
value: docTitle,
},
subsite,
} = fields;
if (subsite) {
const {
value: subsiteID
} = subsite;
if (typeof subsiteID === 'string') {
try {
const req = await fetch(`${process.env.PAYLOAD_PUBLIC_SERVER_URL}/api/subsites/${subsiteID}`);
const doc = await req.json();
if (req.status === 200) {
const {
title: subsiteTitle
} = doc;
return `${base} ${subsiteTitle} - ${docTitle}`;
}
} catch (e) {
console.error(`error while generating meta title, cannot find subsite with id: ${subsiteID}`);
}
}
}
return `${base} - ${docTitle}`
};

View File

@@ -1,14 +0,0 @@
export const stringifyRichText = (richText) => {
let string = '';
richText.forEach((node) => {
const { children } = node;
children.forEach((child) => {
const { text } = child;
string = string.concat(' ', text);
});
});
return string;
};

View File

@@ -1596,11 +1596,6 @@
dependencies:
"@types/node" "*"
"@types/escape-html@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/escape-html/-/escape-html-1.0.1.tgz#b19b4646915f0ae2c306bf984dc0a59c5cfc97ba"
integrity sha512-4mI1FuUUZiuT95fSVqvZxp/ssQK9zsa86S43h9x3zPOSU9BBJ+BfDkXwuaU7BfsD+e7U0/cUUfJFk3iW2M4okA==
"@types/eslint-scope@^3.7.3":
version "3.7.3"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.3.tgz#125b88504b61e3c8bc6f870882003253005c3224"
@@ -2563,14 +2558,14 @@ browser-process-hrtime@^1.0.0:
integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==
browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.6, browserslist@^4.17.5, browserslist@^4.19.1:
version "4.19.1"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.1.tgz#4ac0435b35ab655896c31d53018b6dd5e9e4c9a3"
integrity sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==
version "4.19.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.19.3.tgz#29b7caad327ecf2859485f696f9604214bedd383"
integrity sha512-XK3X4xtKJ+Txj8G5c30B4gsm71s69lqXlkYui4s6EkKxuv49qjYlY6oVd+IFJ73d4YymtM3+djvvt/R/iJwwDg==
dependencies:
caniuse-lite "^1.0.30001286"
electron-to-chromium "^1.4.17"
caniuse-lite "^1.0.30001312"
electron-to-chromium "^1.4.71"
escalade "^3.1.1"
node-releases "^2.0.1"
node-releases "^2.0.2"
picocolors "^1.0.0"
bser@2.1.1:
@@ -2707,7 +2702,7 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297:
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001297, caniuse-lite@^1.0.30001312:
version "1.0.30001312"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001312.tgz#e11eba4b87e24d22697dae05455d5aea28550d5f"
integrity sha512-Wiz1Psk2MEK0pX3rUzWaunLTZzqS2JYZFzNKqAiJGiuxIjRPLgV6+VDPOg6lQOUxmDwhTlh198JsTTi8Hzw6aQ==
@@ -3212,9 +3207,9 @@ css-what@^5.1.0:
integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==
cssdb@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-6.3.1.tgz#d8e521c70b32df082ea5373cdd51ac4dc6b6c151"
integrity sha512-Ho3gIkGY4O8S3J54fHu7RP5GHWz85McDhimaXEwf7qV0MSPhLM0jdd61zqs1kkadIVDAvfqoku0kArbWaMYolw==
version "6.4.0"
resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-6.4.0.tgz#54899b9042e302be3090b8510ea71fefd08c9e6b"
integrity sha512-8NMWrur/ewSNrRNZndbtOTXc2Xb2b+NCTPHj8VErFYvJUlgsMAiBGaFaxG6hjy9zbCjj2ZLwSQrMM+tormO8qA==
cssesc@^3.0.0:
version "3.0.0"
@@ -3635,7 +3630,7 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.4.17:
electron-to-chromium@^1.4.71:
version "1.4.71"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.71.tgz#17056914465da0890ce00351a3b946fd4cd51ff6"
integrity sha512-Hk61vXXKRb2cd3znPE9F+2pLWdIOmP7GjiTj45y6L3W/lO+hSnUSUhq+6lEaERWBdZOHbk2s3YV5c9xVl3boVw==
@@ -3805,7 +3800,7 @@ escalade@^3.1.1:
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
escape-html@^1.0.3, escape-html@~1.0.3:
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
@@ -6682,7 +6677,7 @@ node-notifier@^8.0.0:
uuid "^8.3.0"
which "^2.0.2"
node-releases@^2.0.1:
node-releases@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01"
integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==