From 68c970175abb7dd57a2eb56c421f5b92f99862e4 Mon Sep 17 00:00:00 2001 From: Joshua Chen Date: Wed, 27 Oct 2021 22:38:11 +0800 Subject: [PATCH] refactor: migrate lqip-loader to TS, fix typing for Webpack Loaders (#5779) --- .eslintignore | 1 + .gitignore | 1 + .prettierignore | 1 + packages/docusaurus-mdx-loader/src/index.ts | 34 +++++--- packages/docusaurus-mdx-loader/src/types.d.ts | 15 ++-- .../src/markdownLoader.ts | 20 ++--- .../src/markdown/index.ts | 20 ++--- .../src/markdownLoader.ts | 15 ++-- packages/lqip-loader/package.json | 9 +- .../lqip-loader/src/__tests__/index.test.ts | 6 +- packages/lqip-loader/src/index.js | 83 ------------------- packages/lqip-loader/src/index.ts | 83 +++++++++++++++++++ packages/lqip-loader/src/lqip.js | 75 ----------------- packages/lqip-loader/src/lqip.ts | 52 ++++++++++++ packages/lqip-loader/src/utils.js | 51 ------------ packages/lqip-loader/src/utils.ts | 37 +++++++++ packages/lqip-loader/tsconfig.json | 9 ++ yarn.lock | 7 ++ 18 files changed, 254 insertions(+), 265 deletions(-) delete mode 100644 packages/lqip-loader/src/index.js create mode 100644 packages/lqip-loader/src/index.ts delete mode 100644 packages/lqip-loader/src/lqip.js create mode 100644 packages/lqip-loader/src/lqip.ts delete mode 100644 packages/lqip-loader/src/utils.js create mode 100644 packages/lqip-loader/src/utils.ts create mode 100644 packages/lqip-loader/tsconfig.json diff --git a/.eslintignore b/.eslintignore index d6041c5340..9990df5fb5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -10,6 +10,7 @@ website/ scripts examples/ +packages/lqip-loader/lib/ packages/docusaurus/lib/ packages/docusaurus-*/lib/* packages/docusaurus-*/lib-next/ diff --git a/.gitignore b/.gitignore index 58532e1f02..8e116a2e94 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ test-website test-website-in-workspace packages/create-docusaurus/lib/ +packages/lqip-loader/lib/ packages/docusaurus/lib/ packages/docusaurus-*/lib/* packages/docusaurus-*/lib-next/ diff --git a/.prettierignore b/.prettierignore index 3aceea61eb..d6488c4ee0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,6 +4,7 @@ node_modules build coverage .docusaurus +packages/lqip-loader/lib/ packages/docusaurus/lib/ packages/docusaurus-*/lib/* packages/docusaurus-*/lib-next/ diff --git a/packages/docusaurus-mdx-loader/src/index.ts b/packages/docusaurus-mdx-loader/src/index.ts index 26f98c657a..1695d81b19 100644 --- a/packages/docusaurus-mdx-loader/src/index.ts +++ b/packages/docusaurus-mdx-loader/src/index.ts @@ -22,12 +22,7 @@ import transformImage from './remark/transformImage'; import transformLinks from './remark/transformLinks'; import {getFileLoaderUtils} from '@docusaurus/core/lib/webpack/utils'; import type {RemarkAndRehypePluginOptions} from '@docusaurus/mdx-loader'; - -// TODO temporary until Webpack5 export this type -// see https://github.com/webpack/webpack/issues/11630 -interface Loader extends Function { - (this: any, source: string): Promise; -} +import type {LoaderContext} from 'webpack'; const { loaders: {inlineMarkdownImageFileLoader}, @@ -40,6 +35,19 @@ const DEFAULT_OPTIONS: RemarkAndRehypePluginOptions = { beforeDefaultRehypePlugins: [], }; +type Options = RemarkAndRehypePluginOptions & { + staticDir?: string; + isMDXPartial?: (filePath: string) => boolean; + isMDXPartialFrontMatterWarningDisabled?: boolean; + removeContentTitle?: boolean; + metadataPath?: string | ((filePath: string) => string); + createAssets?: (metadata: { + frontMatter: Record; + metadata: Record; + }) => Record; + filepath: string; +}; + // When this throws, it generally means that there's no metadata file associated with this MDX document // It can happen when using MDX partials (usually starting with _) // That's why it's important to provide the "isMDXPartial" function in config @@ -94,7 +102,10 @@ function createAssetsExportCode(assets: Record) { return `{\n${codeLines.join('\n')}\n}`; } -const docusaurusMdxLoader: Loader = async function (fileString) { +export default async function mdxLoader( + this: LoaderContext, + fileString: string, +): Promise { const callback = this.async(); const filePath = this.resourcePath; const reqOptions = this.getOptions() || {}; @@ -107,7 +118,7 @@ const docusaurusMdxLoader: Loader = async function (fileString) { const hasFrontMatter = Object.keys(frontMatter).length > 0; - const options = { + const options: Options = { ...reqOptions, remarkPlugins: [ ...(reqOptions.beforeDefaultRemarkPlugins || []), @@ -119,7 +130,6 @@ const docusaurusMdxLoader: Loader = async function (fileString) { rehypePlugins: [ ...(reqOptions.beforeDefaultRehypePlugins || []), ...DEFAULT_OPTIONS.rehypePlugins, - ...(reqOptions.rehypePlugins || []), ], filepath: filePath, @@ -129,7 +139,7 @@ const docusaurusMdxLoader: Loader = async function (fileString) { try { result = await mdx(content, options); } catch (err) { - return callback(err); + return callback(err as Error); } // MDX partials are MDX files starting with _ or in a folder starting with _ @@ -195,6 +205,4 @@ ${result} `; return callback(null, code); -}; - -export default docusaurusMdxLoader; +} diff --git a/packages/docusaurus-mdx-loader/src/types.d.ts b/packages/docusaurus-mdx-loader/src/types.d.ts index 01a23fa3be..9de9ebab64 100644 --- a/packages/docusaurus-mdx-loader/src/types.d.ts +++ b/packages/docusaurus-mdx-loader/src/types.d.ts @@ -6,9 +6,11 @@ */ declare module '@docusaurus/mdx-loader' { - type RemarkOrRehypePlugin = - // eslint-disable-next-line @typescript-eslint/ban-types - [Function, Record] | Function; + import type {Plugin} from 'unified'; + + export type RemarkOrRehypePlugin = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [Plugin, Record] | Plugin; export type RemarkAndRehypePluginOptions = { remarkPlugins: RemarkOrRehypePlugin[]; rehypePlugins: RemarkOrRehypePlugin[]; @@ -19,15 +21,16 @@ declare module '@docusaurus/mdx-loader' { // TODO Types provided by MDX 2.0 https://github.com/mdx-js/mdx/blob/main/packages/mdx/types/index.d.ts declare module '@mdx-js/mdx' { - import type {Plugin, Processor} from 'unified'; + import type {Processor} from 'unified'; + import type {RemarkOrRehypePlugin} from '@docusaurus/mdx-loader'; namespace mdx { interface Options { filepath?: string; skipExport?: boolean; wrapExport?: string; - remarkPlugins?: Plugin[]; - rehypePlugins?: Plugin[]; + remarkPlugins?: RemarkOrRehypePlugin[]; + rehypePlugins?: RemarkOrRehypePlugin[]; } function sync(content: string, options?: Options): string; diff --git a/packages/docusaurus-plugin-content-blog/src/markdownLoader.ts b/packages/docusaurus-plugin-content-blog/src/markdownLoader.ts index ecf2701fa8..67535b6cc4 100644 --- a/packages/docusaurus-plugin-content-blog/src/markdownLoader.ts +++ b/packages/docusaurus-plugin-content-blog/src/markdownLoader.ts @@ -8,18 +8,16 @@ import {truncate, linkify} from './blogUtils'; import {parseQuery} from 'loader-utils'; import {BlogMarkdownLoaderOptions} from './types'; +import type {LoaderContext} from 'webpack'; -// TODO temporary until Webpack5 export this type -// see https://github.com/webpack/webpack/issues/11630 -interface Loader extends Function { - (this: any, source: string): string | Buffer | void | undefined; -} - -const markdownLoader: Loader = function (source) { +export default function markdownLoader( + this: LoaderContext, + source: string, +): void { const filePath = this.resourcePath; - const fileString = source as string; + const fileString = source; const callback = this.async(); - const markdownLoaderOptions = this.getOptions() as BlogMarkdownLoaderOptions; + const markdownLoaderOptions = this.getOptions(); // Linkify blog posts let finalContent = linkify({ @@ -38,6 +36,4 @@ const markdownLoader: Loader = function (source) { } return callback && callback(null, finalContent); -}; - -export default markdownLoader; +} diff --git a/packages/docusaurus-plugin-content-docs/src/markdown/index.ts b/packages/docusaurus-plugin-content-docs/src/markdown/index.ts index 683e23e4af..613bed2f63 100644 --- a/packages/docusaurus-plugin-content-docs/src/markdown/index.ts +++ b/packages/docusaurus-plugin-content-docs/src/markdown/index.ts @@ -7,20 +7,16 @@ import {linkify} from './linkify'; import {DocsMarkdownOption} from '../types'; +import type {LoaderContext} from 'webpack'; -// TODO temporary until Webpack5 export this type -// see https://github.com/webpack/webpack/issues/11630 -interface Loader extends Function { - (this: any, source: string): string | Buffer | void | undefined; -} - -const markdownLoader: Loader = function (source) { - const fileString = source as string; +export default function markdownLoader( + this: LoaderContext, + source: string, +): void { + const fileString = source; const callback = this.async(); - const options = this.getOptions() as DocsMarkdownOption; + const options = this.getOptions(); return ( callback && callback(null, linkify(fileString, this.resourcePath, options)) ); -}; - -export default markdownLoader; +} diff --git a/packages/docusaurus-plugin-content-pages/src/markdownLoader.ts b/packages/docusaurus-plugin-content-pages/src/markdownLoader.ts index 5720227c66..a5db152822 100644 --- a/packages/docusaurus-plugin-content-pages/src/markdownLoader.ts +++ b/packages/docusaurus-plugin-content-pages/src/markdownLoader.ts @@ -5,13 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -// TODO temporary until Webpack5 export this type -// see https://github.com/webpack/webpack/issues/11630 -interface Loader extends Function { - (this: any, source: string): string | Buffer | void | undefined; -} +import type {LoaderContext} from 'webpack'; -const markdownLoader: Loader = function (fileString) { +export default function markdownLoader( + this: LoaderContext, + fileString: string, +): void { const callback = this.async(); // const options = this.getOptions(); @@ -20,6 +19,4 @@ const markdownLoader: Loader = function (fileString) { // fileString = linkify(fileString) return callback && callback(null, fileString); -}; - -export default markdownLoader; +} diff --git a/packages/lqip-loader/package.json b/packages/lqip-loader/package.json index d1a030c326..0688337dab 100644 --- a/packages/lqip-loader/package.json +++ b/packages/lqip-loader/package.json @@ -2,10 +2,14 @@ "name": "@docusaurus/lqip-loader", "version": "2.0.0-beta.8", "description": "Low Quality Image Placeholders (LQIP) loader for webpack.", - "main": "src/index.js", + "main": "lib/index.js", "publishConfig": { "access": "public" }, + "scripts": { + "build": "tsc", + "watch": "tsc --watch" + }, "repository": { "type": "git", "url": "https://github.com/facebook/docusaurus.git", @@ -20,5 +24,8 @@ }, "engines": { "node": ">=12.13.0" + }, + "devDependencies": { + "@types/sharp": "^0.29.2" } } diff --git a/packages/lqip-loader/src/__tests__/index.test.ts b/packages/lqip-loader/src/__tests__/index.test.ts index 2107fde7ed..ea3790d6ef 100644 --- a/packages/lqip-loader/src/__tests__/index.test.ts +++ b/packages/lqip-loader/src/__tests__/index.test.ts @@ -10,14 +10,14 @@ import Vibrant from 'node-vibrant'; import {Palette} from 'node-vibrant/lib/color'; import {toPalette, toBase64} from '../utils'; -import lqip from '../lqip'; +import * as lqip from '../lqip'; describe('lqip-loader', () => { describe('toBase64', () => { test('should return a properly formatted Base64 image string', () => { - const expected = 'data:image/jpeg;base64,hello world'; + const expected = 'data:image/jpeg;base64,aGVsbG8gd29ybGQ='; const mockedMimeType = 'image/jpeg'; - const mockedBase64Data = 'hello world'; + const mockedBase64Data = Buffer.from('hello world'); expect(toBase64(mockedMimeType, mockedBase64Data)).toEqual(expected); }); }); diff --git a/packages/lqip-loader/src/index.js b/packages/lqip-loader/src/index.js deleted file mode 100644 index 5210386240..0000000000 --- a/packages/lqip-loader/src/index.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -const lqip = require('./lqip'); - -module.exports = function (contentBuffer) { - if (this.cacheable) { - this.cacheable(); - } - const callback = this.async(); - const imgPath = this.resourcePath; - - const config = this.getOptions() || {}; - config.base64 = 'base64' in config ? config.base64 : true; - config.palette = 'palette' in config ? config.palette : false; - - let content = contentBuffer.toString('utf8'); - const contentIsUrlExport = - /^(?:export default|module.exports =) "data:(.*)base64,(.*)/.test(content); - const contentIsFileExport = /^(?:export default|module.exports =) (.*)/.test( - content, - ); - - let source = ''; - const SOURCE_CHUNK = 1; - - if (contentIsUrlExport) { - source = content.match(/^(?:export default|module.exports =) (.*)/)[ - SOURCE_CHUNK - ]; - } else { - if (!contentIsFileExport) { - // eslint-disable-next-line global-require - const fileLoader = require('file-loader'); - content = fileLoader.call(this, contentBuffer); - } - source = content.match(/^(?:export default|module.exports =) (.*);/)[ - SOURCE_CHUNK - ]; - } - - const outputPromises = []; - - if (config.base64 === true) { - outputPromises.push(lqip.base64(imgPath)); - } else { - outputPromises.push(null); - } - - // color palette generation is set to false by default - // since it is little bit slower than base64 generation - - if (config.palette === true) { - outputPromises.push(lqip.palette(imgPath)); - } else { - outputPromises.push(null); - } - - Promise.all(outputPromises) - .then((data) => { - if (data) { - const [preSrc, palette] = data; - const finalObject = JSON.stringify({src: 'STUB', preSrc, palette}); - const result = `module.exports = ${finalObject.replace( - '"STUB"', - source, - )};`; - callback(null, result); - } else { - callback('ERROR', null); - } - }) - .catch((error) => { - console.error(error); - callback(error, null); - }); -}; - -module.exports.raw = true; diff --git a/packages/lqip-loader/src/index.ts b/packages/lqip-loader/src/index.ts new file mode 100644 index 0000000000..7f20e3b240 --- /dev/null +++ b/packages/lqip-loader/src/index.ts @@ -0,0 +1,83 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as lqip from './lqip'; +import type {LoaderContext} from 'webpack'; + +type Options = { + base64: boolean; + palette: boolean; +}; + +async function lqipLoader( + this: LoaderContext, + contentBuffer: Buffer, +): Promise { + if (this.cacheable) { + this.cacheable(); + } + const callback = this.async(); + const imgPath = this.resourcePath; + + const config = this.getOptions() || {}; + config.base64 = 'base64' in config ? config.base64 : true; + config.palette = 'palette' in config ? config.palette : false; + + let content = contentBuffer.toString('utf8'); + const contentIsUrlExport = + /^(?:export default|module.exports =) "data:(.*)base64,(.*)/.test(content); + const contentIsFileExport = /^(?:export default|module.exports =) (.*)/.test( + content, + ); + + let source = ''; + const SOURCE_CHUNK = 1; + + if (contentIsUrlExport) { + source = content.match(/^(?:export default|module.exports =) (.*)/)![ + SOURCE_CHUNK + ]; + } else { + if (!contentIsFileExport) { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const fileLoader = require('file-loader'); + content = fileLoader.call(this, contentBuffer); + } + source = content.match(/^(?:export default|module.exports =) (.*);/)![ + SOURCE_CHUNK + ]; + } + + const outputPromises: [Promise | null, Promise | null] = [ + config.base64 === true ? lqip.base64(imgPath) : null, + // color palette generation is set to false by default + // since it is little bit slower than base64 generation + config.palette === true ? lqip.palette(imgPath) : null, + ]; + + try { + const data = await Promise.all(outputPromises); + if (data) { + const [preSrc, palette] = data; + const finalObject = JSON.stringify({src: 'STUB', preSrc, palette}); + const result = `module.exports = ${finalObject.replace( + '"STUB"', + source, + )};`; + callback(null, result); + } else { + callback(new Error('ERROR'), undefined); + } + } catch (error) { + console.error(error); + callback(new Error('ERROR'), undefined); + } +} + +lqipLoader.raw = true; + +export default lqipLoader; diff --git a/packages/lqip-loader/src/lqip.js b/packages/lqip-loader/src/lqip.js deleted file mode 100644 index 7c32e0c3d1..0000000000 --- a/packages/lqip-loader/src/lqip.js +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -const Vibrant = require('node-vibrant'); -const path = require('path'); -const sharp = require('sharp'); - -const {version} = require('../package.json'); -const {toPalette, toBase64} = require('./utils'); - -const ERROR_EXT = `Error: Input file is missing or uses unsupported image format, lqip v${version}`; - -const SUPPORTED_MIMES = { - jpeg: 'image/jpeg', - jpg: 'image/jpeg', - png: 'image/png', -}; - -const base64 = (file) => { - return new Promise((resolve, reject) => { - let extension = path.extname(file) || ''; - extension = extension.split('.').pop(); - - if (!SUPPORTED_MIMES[extension]) { - return reject(ERROR_EXT); - } - - return sharp(file) - .resize(10) - .toBuffer() - .then((data) => { - if (data) { - return resolve(toBase64(SUPPORTED_MIMES[extension], data)); - } - return reject( - new Error('Unhandled promise rejection in base64 promise'), - ); - }) - .catch((err) => { - return reject(err); - }); - }); -}; - -const palette = (file) => { - return new Promise((resolve, reject) => { - const vibrant = new Vibrant(file, {}); - vibrant - .getPalette() - .then((pal) => { - if (pal) { - return resolve(toPalette(pal)); - } - return reject( - new Error('Unhandled promise rejection in colorPalette', pal), - ); - }) - .catch((err) => { - return reject(err); - }); - }); -}; - -process.on('unhandledRejection', (up) => { - throw up; -}); - -module.exports = { - base64, - palette, -}; diff --git a/packages/lqip-loader/src/lqip.ts b/packages/lqip-loader/src/lqip.ts new file mode 100644 index 0000000000..7387cdf6cc --- /dev/null +++ b/packages/lqip-loader/src/lqip.ts @@ -0,0 +1,52 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import Vibrant from 'node-vibrant'; +import path from 'path'; +import sharp from 'sharp'; +import {toPalette, toBase64} from './utils'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const {version} = require('../package.json'); + +const ERROR_EXT = `Error: Input file is missing or uses unsupported image format, lqip v${version}`; + +const SUPPORTED_MIMES: Record = { + jpeg: 'image/jpeg', + jpg: 'image/jpeg', + png: 'image/png', +}; + +async function base64(file: string): Promise { + let extension = path.extname(file) || ''; + extension = extension.split('.').pop()!; + + if (!SUPPORTED_MIMES[extension]) { + throw new Error(ERROR_EXT); + } + + const data = await sharp(file).resize(10).toBuffer(); + if (data) { + return toBase64(SUPPORTED_MIMES[extension], data); + } + throw new Error('Unhandled promise rejection in base64 promise'); +} + +async function palette(file: string): Promise { + const vibrant = new Vibrant(file, {}); + const pal = await vibrant.getPalette(); + if (pal) { + return toPalette(pal); + } + throw new Error(`Unhandled promise rejection in colorPalette ${pal}`); +} + +process.on('unhandledRejection', (up) => { + throw up; +}); + +export {base64, palette}; diff --git a/packages/lqip-loader/src/utils.js b/packages/lqip-loader/src/utils.js deleted file mode 100644 index 7c6a85650e..0000000000 --- a/packages/lqip-loader/src/utils.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -// @ts-check - -const {sortBy} = require('lodash'); - -/** - * toBase64 - * @description it returns a Base64 image string with required formatting - * to work on the web ( or in CSS url('..')) - * - * @param {string} extMimeType: image mime type string - * @param data: base64 string - * @returns {string} - */ -const toBase64 = (extMimeType, data) => { - return `data:${extMimeType};base64,${data.toString('base64')}`; -}; - -/** - * toPalette - * @description takes a color swatch object, converts it to an array & returns - * only hex color - * - * @param {import("node-vibrant/lib/color").Palette} swatch - * @returns {string[]} - */ -const toPalette = (swatch) => { - /** @type {Array<{popularity: number, hex: string}>} */ - let palette = Object.keys(swatch).reduce((result, key) => { - if (swatch[key] !== null) { - result.push({ - popularity: swatch[key].getPopulation(), - hex: swatch[key].getHex(), - }); - } - return result; - }, []); - palette = sortBy(palette, ['popularity']); - return palette.map((color) => color.hex).reverse(); -}; - -module.exports = { - toBase64, - toPalette, -}; diff --git a/packages/lqip-loader/src/utils.ts b/packages/lqip-loader/src/utils.ts new file mode 100644 index 0000000000..a4120ca2b6 --- /dev/null +++ b/packages/lqip-loader/src/utils.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import {sortBy} from 'lodash'; +import type {Palette} from 'node-vibrant/lib/color'; + +/** + * it returns a Base64 image string with required formatting + * to work on the web ( or in CSS url('..')) + */ +const toBase64 = (extMimeType: string, data: Buffer): string => { + return `data:${extMimeType};base64,${data.toString('base64')}`; +}; + +/** + * takes a color swatch object, converts it to an array & returns + * only hex color + */ +const toPalette = (swatch: Palette): string[] => { + let palette = Object.keys(swatch).reduce((result, key) => { + if (swatch[key] !== null) { + result.push({ + popularity: swatch[key]!.getPopulation(), + hex: swatch[key]!.getHex(), + }); + } + return result; + }, [] as {popularity: number; hex: string}[]); + palette = sortBy(palette, ['popularity']); + return palette.map((color) => color.hex).reverse(); +}; + +export {toBase64, toPalette}; diff --git a/packages/lqip-loader/tsconfig.json b/packages/lqip-loader/tsconfig.json new file mode 100644 index 0000000000..f5902ba108 --- /dev/null +++ b/packages/lqip-loader/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "incremental": true, + "tsBuildInfoFile": "./lib/.tsbuildinfo", + "rootDir": "src", + "outDir": "lib" + } +} diff --git a/yarn.lock b/yarn.lock index bfceb86164..c724c12fde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4549,6 +4549,13 @@ "@types/mime" "^1" "@types/node" "*" +"@types/sharp@^0.29.2": + version "0.29.2" + resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.29.2.tgz#b4e932e982e258d1013236c8b4bcc14f9883c9a3" + integrity sha512-tIbMvtPa8kMyFMKNhpsPT1HO3CgXLuiCAA8bxHAGAZLyALpYvYc4hUu3pu0+3oExQA5LwvHrWp+OilgXCYVQgg== + dependencies: + "@types/node" "*" + "@types/shelljs@^0.8.6": version "0.8.9" resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.8.9.tgz#45dd8501aa9882976ca3610517dac3831c2fbbf4"