├── .github └── CODEOWNERS ├── .gitignore ├── .vercelignore ├── .yarnrc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── api ├── _fonts │ ├── Inter-Bold.woff2 │ ├── Inter-License.txt │ ├── Inter-Regular.woff2 │ ├── Vera-License.txt │ └── Vera-Mono.woff2 ├── _lib │ ├── chromium.ts │ ├── options.ts │ ├── parser.ts │ ├── sanitizer.ts │ ├── template.ts │ └── types.ts ├── index.ts └── tsconfig.json ├── package.json ├── public ├── archive.html ├── favicon.ico ├── robots.txt ├── style.css └── tweet.png ├── vercel.json ├── web ├── archive.ts └── tsconfig.json └── yarn.lock /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @styfle 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .now 2 | .vercel 3 | node_modules 4 | api/dist 5 | public/dist 6 | 7 | -------------------------------------------------------------------------------- /.vercelignore: -------------------------------------------------------------------------------- 1 | .github 2 | node_modules 3 | api/dist 4 | public/dist 5 | CONTRIBUTING.md 6 | README.md 7 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | save-prefix "" 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | There are three pieces to `og-image` that are worth noting before you begin development. 4 | 5 | 1. The backend image generator located in [/api/index.ts](https://github.com/vercel/og-image/blob/main/api/index.ts) 6 | 2. The html/css template used to generate the image is located in [/_lib/template.ts](https://github.com/vercel/og-image/blob/main/api/_lib/template.ts) 7 | 3. The frontend inputs located in [/web/index.ts](https://github.com/vercel/og-image/blob/main/web/index.ts) 8 | 9 | Vercel handles [routing](https://github.com/vercel/og-image/blob/main/vercel.json#L6) in an elegant way for us so deployment is easy. 10 | 11 | To start hacking, do the following: 12 | 13 | 1. Clone this repo with `git clone https://github.com/vercel/og-image` 14 | 2. Change directory with `cd og-image` 15 | 3. Run `yarn` or `npm install` to install all dependencies 16 | 4. Run locally with `vercel dev` and visit [localhost:3000](http://localhost:3000) (if nothing happens, run `npm install -g vercel`) 17 | 5. If necessary, edit the `exePath` in [options.ts](https://github.com/vercel/og-image/blob/main/api/_lib/options.ts) to point to your local Chrome executable 18 | 19 | Now you're ready to start local development! 20 | 21 | You can set an environment variable to assist with debugging `export OG_HTML_DEBUG=1`. This will render the image as HTML so you can play around with your browser's dev tools before committing changes to the template. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2020 Vercel, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Warning** This repo is outdated and only works with Node.js 14. Please use [@vercel/og](https://vercel.com/blog/introducing-vercel-og-image-generation-fast-dynamic-social-card-images) for new projects. 2 | > 3 | > If you have a problem that reproduces using [the playground](https://og-playground.vercel.app), please create an issue in the [satori](https://github.com/vercel/satori) repo. 4 | > 5 | > For all other issues with `@vercel/og`, please reach out to [Vercel Support](https://vercel.com/help#issues). 6 | -------------------------------------------------------------------------------- /api/_fonts/Inter-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/og-image/d6c78472613b6f63e5c633555041a88385e632d0/api/_fonts/Inter-Bold.woff2 -------------------------------------------------------------------------------- /api/_fonts/Inter-License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2018 The Inter Project Authors (me@rsms.me) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | ----------------------------------------------------------- 8 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 9 | ----------------------------------------------------------- 10 | 11 | PREAMBLE 12 | The goals of the Open Font License (OFL) are to stimulate worldwide 13 | development of collaborative font projects, to support the font creation 14 | efforts of academic and linguistic communities, and to provide a free and 15 | open framework in which fonts may be shared and improved in partnership 16 | with others. 17 | 18 | The OFL allows the licensed fonts to be used, studied, modified and 19 | redistributed freely as long as they are not sold by themselves. The 20 | fonts, including any derivative works, can be bundled, embedded, 21 | redistributed and/or sold with any software provided that any reserved 22 | names are not used by derivative works. The fonts and derivatives, 23 | however, cannot be released under any other type of license. The 24 | requirement for fonts to remain under this license does not apply 25 | to any document created using the fonts or their derivatives. 26 | 27 | DEFINITIONS 28 | "Font Software" refers to the set of files released by the Copyright 29 | Holder(s) under this license and clearly marked as such. This may 30 | include source files, build scripts and documentation. 31 | 32 | "Reserved Font Name" refers to any names specified as such after the 33 | copyright statement(s). 34 | 35 | "Original Version" refers to the collection of Font Software components as 36 | distributed by the Copyright Holder(s). 37 | 38 | "Modified Version" refers to any derivative made by adding to, deleting, 39 | or substituting -- in part or in whole -- any of the components of the 40 | Original Version, by changing formats or by porting the Font Software to a 41 | new environment. 42 | 43 | "Author" refers to any designer, engineer, programmer, technical 44 | writer or other person who contributed to the Font Software. 45 | 46 | PERMISSION AND CONDITIONS 47 | Permission is hereby granted, free of charge, to any person obtaining 48 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 49 | redistribute, and sell modified and unmodified copies of the Font 50 | Software, subject to the following conditions: 51 | 52 | 1) Neither the Font Software nor any of its individual components, 53 | in Original or Modified Versions, may be sold by itself. 54 | 55 | 2) Original or Modified Versions of the Font Software may be bundled, 56 | redistributed and/or sold with any software, provided that each copy 57 | contains the above copyright notice and this license. These can be 58 | included either as stand-alone text files, human-readable headers or 59 | in the appropriate machine-readable metadata fields within text or 60 | binary files as long as those fields can be easily viewed by the user. 61 | 62 | 3) No Modified Version of the Font Software may use the Reserved Font 63 | Name(s) unless explicit written permission is granted by the corresponding 64 | Copyright Holder. This restriction only applies to the primary font name as 65 | presented to the users. 66 | 67 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 68 | Software shall not be used to promote, endorse or advertise any 69 | Modified Version, except to acknowledge the contribution(s) of the 70 | Copyright Holder(s) and the Author(s) or with their explicit written 71 | permission. 72 | 73 | 5) The Font Software, modified or unmodified, in part or in whole, 74 | must be distributed entirely under this license, and must not be 75 | distributed under any other license. The requirement for fonts to 76 | remain under this license does not apply to any document created 77 | using the Font Software. 78 | 79 | TERMINATION 80 | This license becomes null and void if any of the above conditions are 81 | not met. 82 | 83 | DISCLAIMER 84 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 85 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 86 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 87 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 88 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 89 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 90 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 91 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 92 | OTHER DEALINGS IN THE FONT SOFTWARE. 93 | -------------------------------------------------------------------------------- /api/_fonts/Inter-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/og-image/d6c78472613b6f63e5c633555041a88385e632d0/api/_fonts/Inter-Regular.woff2 -------------------------------------------------------------------------------- /api/_fonts/Vera-License.txt: -------------------------------------------------------------------------------- 1 | Bitstream Vera Fonts Copyright 2 | 3 | The fonts have a generous copyright, allowing derivative works (as 4 | long as "Bitstream" or "Vera" are not in the names), and full 5 | redistribution (so long as they are not *sold* by themselves). They 6 | can be be bundled, redistributed and sold with any software. 7 | 8 | The fonts are distributed under the following copyright: 9 | 10 | Copyright 11 | ========= 12 | 13 | Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream 14 | Vera is a trademark of Bitstream, Inc. 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining 17 | a copy of the fonts accompanying this license ("Fonts") and associated 18 | documentation files (the "Font Software"), to reproduce and distribute 19 | the Font Software, including without limitation the rights to use, 20 | copy, merge, publish, distribute, and/or sell copies of the Font 21 | Software, and to permit persons to whom the Font Software is furnished 22 | to do so, subject to the following conditions: 23 | 24 | The above copyright and trademark notices and this permission notice 25 | shall be included in all copies of one or more of the Font Software 26 | typefaces. 27 | 28 | The Font Software may be modified, altered, or added to, and in 29 | particular the designs of glyphs or characters in the Fonts may be 30 | modified and additional glyphs or characters may be added to the 31 | Fonts, only if the fonts are renamed to names not containing either 32 | the words "Bitstream" or the word "Vera". 33 | 34 | This License becomes null and void to the extent applicable to Fonts 35 | or Font Software that has been modified and is distributed under the 36 | "Bitstream Vera" names. 37 | 38 | The Font Software may be sold as part of a larger software package but 39 | no copy of one or more of the Font Software typefaces may be sold by 40 | itself. 41 | 42 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 43 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 44 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 45 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL 46 | BITSTREAM OR THE GNOME FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR 47 | OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, 48 | OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR 49 | OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT 50 | SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. 51 | 52 | Except as contained in this notice, the names of Gnome, the Gnome 53 | Foundation, and Bitstream Inc., shall not be used in advertising or 54 | otherwise to promote the sale, use or other dealings in this Font 55 | Software without prior written authorization from the Gnome Foundation 56 | or Bitstream Inc., respectively. For further information, contact: 57 | fonts at gnome dot org. 58 | 59 | Copyright FAQ 60 | ============= 61 | 62 | 1. I don't understand the resale restriction... What gives? 63 | 64 | Bitstream is giving away these fonts, but wishes to ensure its 65 | competitors can't just drop the fonts as is into a font sale system 66 | and sell them as is. It seems fair that if Bitstream can't make money 67 | from the Bitstream Vera fonts, their competitors should not be able to 68 | do so either. You can sell the fonts as part of any software package, 69 | however. 70 | 71 | 2. I want to package these fonts separately for distribution and 72 | sale as part of a larger software package or system. Can I do so? 73 | 74 | Yes. A RPM or Debian package is a "larger software package" to begin 75 | with, and you aren't selling them independently by themselves. 76 | See 1. above. 77 | 78 | 3. Are derivative works allowed? 79 | Yes! 80 | 81 | 4. Can I change or add to the font(s)? 82 | Yes, but you must change the name(s) of the font(s). 83 | 84 | 5. Under what terms are derivative works allowed? 85 | 86 | You must change the name(s) of the fonts. This is to ensure the 87 | quality of the fonts, both to protect Bitstream and Gnome. We want to 88 | ensure that if an application has opened a font specifically of these 89 | names, it gets what it expects (though of course, using fontconfig, 90 | substitutions could still could have occurred during font 91 | opening). You must include the Bitstream copyright. Additional 92 | copyrights can be added, as per copyright law. Happy Font Hacking! 93 | 94 | 6. If I have improvements for Bitstream Vera, is it possible they might get 95 | adopted in future versions? 96 | 97 | Yes. The contract between the Gnome Foundation and Bitstream has 98 | provisions for working with Bitstream to ensure quality additions to 99 | the Bitstream Vera font family. Please contact us if you have such 100 | additions. Note, that in general, we will want such additions for the 101 | entire family, not just a single font, and that you'll have to keep 102 | both Gnome and Jim Lyles, Vera's designer, happy! To make sense to add 103 | glyphs to the font, they must be stylistically in keeping with Vera's 104 | design. Vera cannot become a "ransom note" font. Jim Lyles will be 105 | providing a document describing the design elements used in Vera, as a 106 | guide and aid for people interested in contributing to Vera. 107 | 108 | 7. I want to sell a software package that uses these fonts: Can I do so? 109 | 110 | Sure. Bundle the fonts with your software and sell your software 111 | with the fonts. That is the intent of the copyright. 112 | 113 | 8. If applications have built the names "Bitstream Vera" into them, 114 | can I override this somehow to use fonts of my choosing? 115 | 116 | This depends on exact details of the software. Most open source 117 | systems and software (e.g., Gnome, KDE, etc.) are now converting to 118 | use fontconfig (see www.fontconfig.org) to handle font configuration, 119 | selection and substitution; it has provisions for overriding font 120 | names and subsituting alternatives. An example is provided by the 121 | supplied local.conf file, which chooses the family Bitstream Vera for 122 | "sans", "serif" and "monospace". Other software (e.g., the XFree86 123 | core server) has other mechanisms for font substitution. 124 | 125 | -------------------------------------------------------------------------------- /api/_fonts/Vera-Mono.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/og-image/d6c78472613b6f63e5c633555041a88385e632d0/api/_fonts/Vera-Mono.woff2 -------------------------------------------------------------------------------- /api/_lib/chromium.ts: -------------------------------------------------------------------------------- 1 | import core from 'puppeteer-core'; 2 | import { getOptions } from './options'; 3 | import { FileType } from './types'; 4 | let _page: core.Page | null; 5 | 6 | async function getPage(isDev: boolean) { 7 | if (_page) { 8 | return _page; 9 | } 10 | const options = await getOptions(isDev); 11 | const browser = await core.launch(options); 12 | _page = await browser.newPage(); 13 | return _page; 14 | } 15 | 16 | export async function getScreenshot(html: string, type: FileType, isDev: boolean) { 17 | const page = await getPage(isDev); 18 | await page.setViewport({ width: 2048, height: 1170 }); 19 | await page.setContent(html); 20 | const file = await page.screenshot({ type }); 21 | return file; 22 | } 23 | -------------------------------------------------------------------------------- /api/_lib/options.ts: -------------------------------------------------------------------------------- 1 | import chrome from 'chrome-aws-lambda'; 2 | const exePath = process.platform === 'win32' 3 | ? 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe' 4 | : process.platform === 'linux' 5 | ? '/usr/bin/google-chrome' 6 | : '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; 7 | 8 | interface Options { 9 | args: string[]; 10 | executablePath: string; 11 | headless: boolean; 12 | } 13 | 14 | export async function getOptions(isDev: boolean) { 15 | let options: Options; 16 | if (isDev) { 17 | options = { 18 | args: [], 19 | executablePath: exePath, 20 | headless: true 21 | }; 22 | } else { 23 | options = { 24 | args: chrome.args, 25 | executablePath: await chrome.executablePath, 26 | headless: chrome.headless, 27 | }; 28 | } 29 | return options; 30 | } 31 | -------------------------------------------------------------------------------- /api/_lib/parser.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage } from 'http'; 2 | import { parse } from 'url'; 3 | import { ParsedRequest, Theme } from './types'; 4 | 5 | export function parseRequest(req: IncomingMessage) { 6 | console.log('HTTP ' + req.url); 7 | const { pathname, query } = parse(req.url || '/', true); 8 | const { fontSize, images, widths, heights, theme, md } = (query || {}); 9 | 10 | if (Array.isArray(fontSize)) { 11 | throw new Error('Expected a single fontSize'); 12 | } 13 | if (Array.isArray(theme)) { 14 | throw new Error('Expected a single theme'); 15 | } 16 | 17 | const arr = (pathname || '/').slice(1).split('.'); 18 | let extension = ''; 19 | let text = ''; 20 | if (arr.length === 0) { 21 | text = ''; 22 | } else if (arr.length === 1) { 23 | text = arr[0]; 24 | } else { 25 | extension = arr.pop() as string; 26 | text = arr.join('.'); 27 | } 28 | 29 | const parsedRequest: ParsedRequest = { 30 | fileType: extension === 'jpeg' ? extension : 'png', 31 | text: decodeURIComponent(text), 32 | theme: theme === 'dark' ? 'dark' : 'light', 33 | md: md === '1' || md === 'true', 34 | fontSize: fontSize || '96px', 35 | images: getArray(images), 36 | widths: getArray(widths), 37 | heights: getArray(heights), 38 | }; 39 | parsedRequest.images = getDefaultImages(parsedRequest.images, parsedRequest.theme); 40 | return parsedRequest; 41 | } 42 | 43 | function getArray(stringOrArray: string[] | string | undefined): string[] { 44 | if (typeof stringOrArray === 'undefined') { 45 | return []; 46 | } else if (Array.isArray(stringOrArray)) { 47 | return stringOrArray; 48 | } else { 49 | return [stringOrArray]; 50 | } 51 | } 52 | 53 | function getDefaultImages(images: string[], theme: Theme): string[] { 54 | const defaultImage = theme === 'light' 55 | ? 'https://assets.vercel.com/image/upload/front/assets/design/vercel-triangle-black.svg' 56 | : 'https://assets.vercel.com/image/upload/front/assets/design/vercel-triangle-white.svg'; 57 | 58 | if (!images || !images[0]) { 59 | return [defaultImage]; 60 | } 61 | if (!images[0].startsWith('https://assets.vercel.com/') && !images[0].startsWith('https://assets.zeit.co/')) { 62 | images[0] = defaultImage; 63 | } 64 | return images; 65 | } 66 | -------------------------------------------------------------------------------- /api/_lib/sanitizer.ts: -------------------------------------------------------------------------------- 1 | const entityMap: { [key: string]: string } = { 2 | "&": "&", 3 | "<": "<", 4 | ">": ">", 5 | '"': '"', 6 | "'": ''', 7 | "/": '/' 8 | }; 9 | 10 | export function sanitizeHtml(html: string) { 11 | return String(html).replace(/[&<>"'\/]/g, key => entityMap[key]); 12 | } 13 | 14 | -------------------------------------------------------------------------------- /api/_lib/template.ts: -------------------------------------------------------------------------------- 1 | 2 | import { readFileSync } from 'fs'; 3 | import { sanitizeHtml } from './sanitizer'; 4 | import { ParsedRequest } from './types'; 5 | const twemoji = require('twemoji'); 6 | const twOptions = { folder: 'svg', ext: '.svg' }; 7 | const emojify = (text: string) => twemoji.parse(text, twOptions); 8 | 9 | const rglr = readFileSync(`${__dirname}/../_fonts/Inter-Regular.woff2`).toString('base64'); 10 | const bold = readFileSync(`${__dirname}/../_fonts/Inter-Bold.woff2`).toString('base64'); 11 | const mono = readFileSync(`${__dirname}/../_fonts/Vera-Mono.woff2`).toString('base64'); 12 | 13 | function getCss(theme: string, fontSize: string) { 14 | let background = 'white'; 15 | let foreground = 'black'; 16 | let radial = 'lightgray'; 17 | 18 | if (theme === 'dark') { 19 | background = 'black'; 20 | foreground = 'white'; 21 | radial = 'dimgray'; 22 | } 23 | return ` 24 | @font-face { 25 | font-family: 'Inter'; 26 | font-style: normal; 27 | font-weight: normal; 28 | src: url(data:font/woff2;charset=utf-8;base64,${rglr}) format('woff2'); 29 | } 30 | 31 | @font-face { 32 | font-family: 'Inter'; 33 | font-style: normal; 34 | font-weight: bold; 35 | src: url(data:font/woff2;charset=utf-8;base64,${bold}) format('woff2'); 36 | } 37 | 38 | @font-face { 39 | font-family: 'Vera'; 40 | font-style: normal; 41 | font-weight: normal; 42 | src: url(data:font/woff2;charset=utf-8;base64,${mono}) format("woff2"); 43 | } 44 | 45 | body { 46 | background: ${background}; 47 | background-image: radial-gradient(circle at 25px 25px, ${radial} 2%, transparent 0%), radial-gradient(circle at 75px 75px, ${radial} 2%, transparent 0%); 48 | background-size: 100px 100px; 49 | height: 100vh; 50 | display: flex; 51 | text-align: center; 52 | align-items: center; 53 | justify-content: center; 54 | } 55 | 56 | code { 57 | color: #D400FF; 58 | font-family: 'Vera'; 59 | white-space: pre-wrap; 60 | letter-spacing: -5px; 61 | } 62 | 63 | code:before, code:after { 64 | content: '\`'; 65 | } 66 | 67 | .logo-wrapper { 68 | display: flex; 69 | align-items: center; 70 | align-content: center; 71 | justify-content: center; 72 | justify-items: center; 73 | } 74 | 75 | .logo { 76 | margin: 0 75px; 77 | } 78 | 79 | .plus { 80 | color: #BBB; 81 | font-family: Times New Roman, Verdana; 82 | font-size: 100px; 83 | } 84 | 85 | .spacer { 86 | margin: 150px; 87 | } 88 | 89 | .emoji { 90 | height: 1em; 91 | width: 1em; 92 | margin: 0 .05em 0 .1em; 93 | vertical-align: -0.1em; 94 | } 95 | 96 | .heading { 97 | font-family: 'Inter', sans-serif; 98 | font-size: ${sanitizeHtml(fontSize)}; 99 | font-style: normal; 100 | color: ${foreground}; 101 | line-height: 1.8; 102 | }`; 103 | } 104 | 105 | export function getHtml(parsedReq: ParsedRequest) { 106 | const { text, theme, md, fontSize, images, widths, heights } = parsedReq; 107 | let html = sanitizeHtml(text); 108 | if (md) { 109 | html = html.replace(/\*\*(.+)\*\*/g, (_, match) => `${match}`) 110 | html = html.replace(/__(.+)__/g, (_, match) => `${match}`) 111 | html = html.replace(/\*(.+)\*/g, (_, match) => `${match}`) 112 | html = html.replace(/_(.+)_/g, (_, match) => `${match}`) 113 | } 114 | return ` 115 | 116 | 117 | Generated Image 118 | 119 | 122 | 123 |
124 |
125 |
126 | ${images.map((img, i) => 127 | getPlusSign(i) + getImage(img, widths[i], heights[i]) 128 | ).join('')} 129 |
130 |
131 |
${emojify(html)} 132 |
133 |
134 | 135 | `; 136 | } 137 | 138 | function getImage(src: string, width ='auto', height = '225') { 139 | return `` 146 | } 147 | 148 | function getPlusSign(i: number) { 149 | return i === 0 ? '' : '
+
'; 150 | } 151 | -------------------------------------------------------------------------------- /api/_lib/types.ts: -------------------------------------------------------------------------------- 1 | export type FileType = 'png' | 'jpeg'; 2 | export type Theme = 'light' | 'dark'; 3 | 4 | export interface ParsedRequest { 5 | fileType: FileType; 6 | text: string; 7 | theme: Theme; 8 | md: boolean; 9 | fontSize: string; 10 | images: string[]; 11 | widths: string[]; 12 | heights: string[]; 13 | } 14 | -------------------------------------------------------------------------------- /api/index.ts: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ServerResponse } from 'http'; 2 | import { parseRequest } from './_lib/parser'; 3 | import { getScreenshot } from './_lib/chromium'; 4 | import { getHtml } from './_lib/template'; 5 | 6 | const isDev = !process.env.AWS_REGION; 7 | const isHtmlDebug = process.env.OG_HTML_DEBUG === '1'; 8 | 9 | export default async function handler(req: IncomingMessage, res: ServerResponse) { 10 | try { 11 | const parsedReq = parseRequest(req); 12 | const html = getHtml(parsedReq); 13 | if (isHtmlDebug) { 14 | res.setHeader('Content-Type', 'text/html'); 15 | res.end(html); 16 | return; 17 | } 18 | const { fileType } = parsedReq; 19 | const file = await getScreenshot(html, fileType, isDev); 20 | res.statusCode = 200; 21 | res.setHeader('Content-Type', `image/${fileType}`); 22 | res.setHeader('Cache-Control', `public, immutable, no-transform, s-maxage=31536000, max-age=31536000`); 23 | res.end(file); 24 | } catch (e) { 25 | res.statusCode = 500; 26 | res.setHeader('Content-Type', 'text/html'); 27 | res.end('

Internal Error

Sorry, there was a problem

'); 28 | console.error(e); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "target": "esnext", 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "sourceMap": true, 9 | "strict": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitReturns": true, 12 | "noEmitOnError": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "removeComments": true, 16 | "preserveConstEnums": true, 17 | "esModuleInterop": true 18 | }, 19 | "include": [ 20 | "./" 21 | ] 22 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "engines": { 4 | "node": "14.x" 5 | }, 6 | "scripts": { 7 | "build": "tsc -p web/tsconfig.json" 8 | }, 9 | "dependencies": { 10 | "chrome-aws-lambda": "7.0.0", 11 | "puppeteer-core": "13.1.2", 12 | "twemoji": "13.0.1" 13 | }, 14 | "devDependencies": { 15 | "@types/puppeteer": "5.4.4", 16 | "@types/puppeteer-core": "5.4.0", 17 | "typescript": "4.1.5" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/archive.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Open Graph Image as a Service 20 | 21 | 22 | 23 | 24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |

Open Graph Image as a Service

35 |
36 | Loading... 37 |
38 |
39 |

What is this?

40 |

This is a service that generates dynamic Open Graph images that you can embed in your <meta> tags.

41 |

For each keystroke, headless chromium is used to render an HTML page and take a screenshot of the result which gets cached.

42 |

Find out how this works and deploy your own image generator by visiting GitHub.

43 | 44 |
45 |
46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/og-image/d6c78472613b6f63e5c633555041a88385e632d0/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "SF Pro Text", "SF Pro Icons", "Helvetica Neue", "Helvetica", 3 | "Arial", sans-serif; 4 | margin: 20px; 5 | overflow-x: hidden; 6 | padding: 0; 7 | box-sizing: border-box; 8 | } 9 | 10 | a { 11 | cursor: pointer; 12 | color: #0076FF; 13 | text-decoration: none; 14 | transition: all 0.2s ease; 15 | border-bottom: 1px solid white; 16 | } 17 | 18 | a:hover { 19 | border-bottom: 1px solid #0076FF; 20 | } 21 | 22 | footer { 23 | opacity: 0.5; 24 | font-size: 0.8em; 25 | } 26 | 27 | .center, h1 { 28 | text-align: center; 29 | } 30 | 31 | .container { 32 | display: flex; 33 | justify-content: center; 34 | width: 100%; 35 | } 36 | 37 | .split { 38 | display: flex; 39 | justify-content: center; 40 | width: 100%; 41 | } 42 | 43 | .pull-left { 44 | min-width: 50%; 45 | display: flex; 46 | justify-content: flex-end; 47 | } 48 | 49 | .pull-right { 50 | min-width: 50%; 51 | } 52 | 53 | button { 54 | appearance: none; 55 | align-items: center; 56 | color: #fff; 57 | background: #000; 58 | display: inline-flex; 59 | width: 100px; 60 | height: 40px; 61 | padding: 0 25px; 62 | outline: none; 63 | border: 1px solid #000; 64 | font-size: 12px; 65 | justify-content: center; 66 | text-transform: uppercase; 67 | cursor: pointer; 68 | text-align: center; 69 | user-select: none; 70 | font-weight: 100; 71 | position: relative; 72 | overflow: hidden; 73 | transition: border 0.2s,background 0.2s,color 0.2s ease-out; 74 | border-radius: 5px; 75 | white-space: nowrap; 76 | text-decoration: none; 77 | line-height: 0; 78 | 79 | height: 24px; 80 | width: calc(100% + 2px); 81 | padding: 0 10px; 82 | font-size: 12px; 83 | background: #fff; 84 | border: 1px solid #eaeaea; 85 | color: #666; 86 | } 87 | 88 | button:hover { 89 | color: #000; 90 | border-color: #000; 91 | background: #fff; 92 | } 93 | 94 | .input-outer-wrapper { 95 | align-items: center; 96 | border-radius: 5px; 97 | border: 1px solid #e1e1e1; 98 | display: inline-flex; 99 | height: 37px; 100 | position: relative; 101 | transition: border 0.2s ease,color 0.2s ease; 102 | vertical-align: middle; 103 | width: 100%; 104 | } 105 | .input-outer-wrapper.small { 106 | height: 24px; 107 | min-width: 100px; 108 | width: 100px; 109 | } 110 | 111 | .input-outer-wrapper:hover { 112 | box-shadow: 0 2px 6px rgba(0,0,0,0.1); 113 | border-color: #ddd; 114 | } 115 | 116 | .input-inner-wrapper { 117 | display: block; 118 | margin: 4px 10px; 119 | position: relative; 120 | width: 100%; 121 | } 122 | 123 | .input-inner-wrapper input { 124 | background-color: transparent; 125 | border-radius: 0; 126 | border: none; 127 | box-shadow: none; 128 | box-sizing: border-box; 129 | display: block; 130 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 131 | font-size: 14px; 132 | line-height: 27px; 133 | outline: 0; 134 | width: 100%; 135 | } 136 | 137 | .select-wrapper { 138 | appearance: none; 139 | color: #000; 140 | background: #fff; 141 | display: inline-flex; 142 | height: 40px; 143 | outline: none; 144 | border: 1px solid #eaeaea; 145 | font-size: 12px; 146 | text-transform: uppercase; 147 | user-select: none; 148 | font-weight: 100; 149 | position: relative; 150 | overflow: hidden; 151 | transition: border 0.2s,background 0.2s,color 0.2s ease-out, box-shadow 0.2s ease; 152 | border-radius: 5px; 153 | white-space: nowrap; 154 | line-height: 0; 155 | width: auto; 156 | min-width: 100%; 157 | } 158 | 159 | .select-wrapper:hover { 160 | box-shadow: 0 2px 6px rgba(0,0,0,0.1); 161 | border-color: #ddd; 162 | } 163 | 164 | .select-wrapper.small { 165 | height: 24px; 166 | min-width: 100px; 167 | width: 100px; 168 | } 169 | 170 | .select-arrow { 171 | border-left: 1px solid #eaeaea; 172 | background: #fff; 173 | width: 40px; 174 | height: 100%; 175 | position: absolute; 176 | right: 0; 177 | pointer-events: none; 178 | display: flex; 179 | align-items: center; 180 | justify-content: center; 181 | } 182 | 183 | .select-arrow.small { 184 | width: 22px; 185 | } 186 | 187 | .svg { 188 | fill: #151513; 189 | color: #fff; 190 | position: absolute; 191 | top: 0; 192 | border: 0; 193 | right: 0; 194 | } 195 | select { 196 | height: 100%; 197 | border: none; 198 | box-shadow: none; 199 | background: transparent; 200 | background-image: none; 201 | color: #000; 202 | line-height: 40px; 203 | font-size: 14px; 204 | margin-right: -20px; 205 | width: calc(100% + 20px); 206 | padding: 0 0 0 16px; 207 | text-transform: none; 208 | } 209 | 210 | .field-flex { 211 | display: flex; 212 | margin-top: 10px; 213 | } 214 | 215 | .field-value { 216 | margin: 10px 80px; 217 | } 218 | 219 | .field-label { 220 | display: inline-block; 221 | width: 100px; 222 | margin-right: 20px; 223 | text-align: right; 224 | } 225 | 226 | .field-value { 227 | width: 200px; 228 | display: inline-block; 229 | } 230 | 231 | label { 232 | display: flex; 233 | align-items: center; 234 | } 235 | 236 | .toast-area { 237 | position: fixed; 238 | bottom: 10px; 239 | right: 20px; 240 | max-width: 420px; 241 | z-index: 2000; 242 | transition: transform 0.4s ease; 243 | } 244 | 245 | .toast-outer { 246 | width: 420px; 247 | height: 72px; 248 | position: absolute; 249 | bottom: 0; 250 | right: 0; 251 | transition: all 0.4s ease; 252 | transform: translate3d(0,130%,0px) scale(1); 253 | animation: show-jsx-1861505484 0.4s ease forwards; 254 | opacity: 1; 255 | } 256 | 257 | .toast-inner { 258 | width: 420px; 259 | background: black; 260 | color: white; 261 | border: 0; 262 | border-radius: 5px; 263 | height: 60px; 264 | align-items: center; 265 | justify-content: space-between; 266 | 267 | box-shadow: 0 4px 9px rgba(0,0,0,0.12); 268 | font-size: 14px; 269 | display: flex; 270 | } 271 | 272 | .toast-message { 273 | text-overflow: ellipsis; 274 | white-space: nowrap; 275 | width: 100%; 276 | overflow: hidden; 277 | margin-top: -1px; 278 | margin-left: 20px; 279 | } 280 | 281 | img { 282 | max-width: 100%; 283 | transition: all 0.3s ease-in 0s; 284 | } 285 | 286 | .image-wrapper { 287 | display: block; 288 | cursor: pointer; 289 | text-decoration: none; 290 | background: #fff; 291 | box-shadow: 0px 1px 5px 0px rgba(0,0,0,0.12); 292 | border-radius: 5px; 293 | margin-bottom: 50px; 294 | transition: all 0.2s ease; 295 | max-width: 100%; 296 | } 297 | 298 | .image-wrapper:hover { 299 | box-shadow: 0 1px 16px rgba(0,0,0,0.1); 300 | border-color: #ddd; 301 | } 302 | 303 | 304 | @media (max-width: 1000px) { 305 | .field { 306 | margin: 20px 10px; 307 | } 308 | .pull-left { 309 | margin-left: 1.5em; 310 | } 311 | } 312 | 313 | @media (max-width: 900px) { 314 | .split { 315 | flex-wrap: wrap; 316 | } 317 | 318 | .field-label { 319 | width: 60px; 320 | font-size: 13px; 321 | } 322 | } 323 | 324 | .github-corner:hover .octo-arm { 325 | animation: octocat-wave 560ms ease-in-out; 326 | } 327 | 328 | @keyframes octocat-wave { 329 | 0%,100% { transform:rotate(0) } 330 | 20%,60% { transform:rotate(-25deg) } 331 | 40%,80% { transform:rotate(10deg) } 332 | } 333 | 334 | @media (max-width: 500px) { 335 | .github-corner:hover .octo-arm { 336 | animation: none; 337 | } 338 | .github-corner .octo-arm { 339 | animation: octocat-wave 560ms ease-in-out; 340 | } 341 | .container { 342 | margin: 19%; 343 | } 344 | .svg { 345 | right: -33%; 346 | } 347 | } 348 | @media (max-width: 360px) { 349 | .container { 350 | margin: 29.5%; 351 | } 352 | .svg { 353 | right: -52%; 354 | } 355 | } 356 | 357 | input[type=number]::-webkit-inner-spin-button, 358 | input[type=number]::-webkit-outer-spin-button { 359 | -webkit-appearance: none; 360 | -moz-appearance: none; 361 | appearance: none; 362 | margin: 0; 363 | } 364 | -------------------------------------------------------------------------------- /public/tweet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/og-image/d6c78472613b6f63e5c633555041a88385e632d0/public/tweet.png -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "redirects": [ 4 | { "source": "/", "destination": "https://og-playground.vercel.app" } 5 | ], 6 | "rewrites": [ 7 | { "source": "/(.+)", "destination": "/api" } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /web/archive.ts: -------------------------------------------------------------------------------- 1 | import type { ParsedRequest, Theme, FileType } from '../api/_lib/types'; 2 | const { H, R, copee } = (window as any); 3 | let timeout = -1; 4 | 5 | interface ImagePreviewProps { 6 | src: string; 7 | onclick: () => void; 8 | onload: () => void; 9 | onerror: () => void; 10 | loading: boolean; 11 | } 12 | 13 | const ImagePreview = ({ src, onclick, onload, onerror, loading }: ImagePreviewProps) => { 14 | const style = { 15 | filter: loading ? 'blur(5px)' : '', 16 | opacity: loading ? 0.1 : 1, 17 | }; 18 | const title = 'Click to copy image URL to clipboard'; 19 | return H('a', 20 | { className: 'image-wrapper', href: src, onclick }, 21 | H('img', 22 | { src, onload, onerror, style, title } 23 | ) 24 | ); 25 | } 26 | 27 | interface DropdownOption { 28 | text: string; 29 | value: string; 30 | } 31 | 32 | interface DropdownProps { 33 | options: DropdownOption[]; 34 | value: string; 35 | onchange: (val: string) => void; 36 | small: boolean; 37 | } 38 | 39 | const Dropdown = ({ options, value, onchange, small }: DropdownProps) => { 40 | const wrapper = small ? 'select-wrapper small' : 'select-wrapper'; 41 | const arrow = small ? 'select-arrow small' : 'select-arrow'; 42 | return H('div', 43 | { className: wrapper }, 44 | H('select', 45 | { onchange: (e: any) => onchange(e.target.value) }, 46 | options.map(o => 47 | H('option', 48 | { value: o.value, selected: value === o.value }, 49 | o.text 50 | ) 51 | ) 52 | ), 53 | H('div', 54 | { className: arrow }, 55 | '▼' 56 | ) 57 | ); 58 | } 59 | 60 | interface TextInputProps { 61 | value: string; 62 | oninput: (val: string) => void; 63 | small: boolean; 64 | placeholder?: string; 65 | type?: string 66 | } 67 | 68 | const TextInput = ({ value, oninput, small, type = 'text', placeholder = '' }: TextInputProps) => { 69 | return H('div', 70 | { className: 'input-outer-wrapper' + (small ? ' small' : '') }, 71 | H('div', 72 | { className: 'input-inner-wrapper' }, 73 | H('input', 74 | { type, value, placeholder, oninput: (e: any) => oninput(e.target.value) } 75 | ) 76 | ) 77 | ); 78 | } 79 | 80 | interface ButtonProps { 81 | label: string; 82 | onclick: () => void; 83 | } 84 | 85 | const Button = ({ label, onclick }: ButtonProps) => { 86 | return H('button', { onclick }, label); 87 | } 88 | 89 | interface FieldProps { 90 | label: string; 91 | input: any; 92 | } 93 | 94 | const Field = ({ label, input }: FieldProps) => { 95 | return H('div', 96 | { className: 'field' }, 97 | H('label', 98 | H('div', {className: 'field-label'}, label), 99 | H('div', { className: 'field-value' }, input), 100 | ), 101 | ); 102 | } 103 | 104 | interface ToastProps { 105 | show: boolean; 106 | message: string; 107 | } 108 | 109 | const Toast = ({ show, message }: ToastProps) => { 110 | const style = { transform: show ? 'translate3d(0,-0px,-0px) scale(1)' : '' }; 111 | return H('div', 112 | { className: 'toast-area' }, 113 | H('div', 114 | { className: 'toast-outer', style }, 115 | H('div', 116 | { className: 'toast-inner' }, 117 | H('div', 118 | { className: 'toast-message'}, 119 | message 120 | ) 121 | ) 122 | ), 123 | ); 124 | } 125 | 126 | const themeOptions: DropdownOption[] = [ 127 | { text: 'Light', value: 'light' }, 128 | { text: 'Dark', value: 'dark' }, 129 | ]; 130 | 131 | const fileTypeOptions: DropdownOption[] = [ 132 | { text: 'PNG', value: 'png' }, 133 | { text: 'JPEG', value: 'jpeg' }, 134 | ]; 135 | 136 | const fontSizeOptions: DropdownOption[] = Array 137 | .from({ length: 10 }) 138 | .map((_, i) => i * 25) 139 | .filter(n => n > 0) 140 | .map(n => ({ text: n + 'px', value: n + 'px' })); 141 | 142 | const markdownOptions: DropdownOption[] = [ 143 | { text: 'Plain Text', value: '0' }, 144 | { text: 'Markdown', value: '1' }, 145 | ]; 146 | 147 | const imageLightOptions: DropdownOption[] = [ 148 | { text: 'Vercel', value: 'https://assets.vercel.com/image/upload/front/assets/design/vercel-triangle-black.svg' }, 149 | { text: 'Next.js', value: 'https://assets.vercel.com/image/upload/front/assets/design/nextjs-black-logo.svg' }, 150 | { text: 'Hyper', value: 'https://assets.vercel.com/image/upload/front/assets/design/hyper-color-logo.svg' }, 151 | ]; 152 | 153 | const imageDarkOptions: DropdownOption[] = [ 154 | 155 | { text: 'Vercel', value: 'https://assets.vercel.com/image/upload/front/assets/design/vercel-triangle-white.svg' }, 156 | { text: 'Next.js', value: 'https://assets.vercel.com/image/upload/front/assets/design/nextjs-white-logo.svg' }, 157 | { text: 'Hyper', value: 'https://assets.vercel.com/image/upload/front/assets/design/hyper-bw-logo.svg' }, 158 | ]; 159 | 160 | 161 | interface AppState extends ParsedRequest { 162 | loading: boolean; 163 | showToast: boolean; 164 | messageToast: string; 165 | selectedImageIndex: number; 166 | widths: string[]; 167 | heights: string[]; 168 | overrideUrl: URL | null; 169 | } 170 | 171 | type SetState = (state: Partial) => void; 172 | 173 | const App = (_: any, state: AppState, setState: SetState) => { 174 | const setLoadingState = (newState: Partial) => { 175 | window.clearTimeout(timeout); 176 | if (state.overrideUrl && state.overrideUrl !== newState.overrideUrl) { 177 | newState.overrideUrl = state.overrideUrl; 178 | } 179 | if (newState.overrideUrl) { 180 | timeout = window.setTimeout(() => setState({ overrideUrl: null }), 200); 181 | } 182 | 183 | setState({ ...newState, loading: true }); 184 | }; 185 | const { 186 | fileType = 'png', 187 | fontSize = '100px', 188 | theme = 'light', 189 | md = true, 190 | text = '**Hello** World', 191 | images=[imageLightOptions[0].value], 192 | widths=[], 193 | heights=[], 194 | showToast = false, 195 | messageToast = '', 196 | loading = true, 197 | selectedImageIndex = 0, 198 | overrideUrl = null, 199 | } = state; 200 | 201 | const mdValue = md ? '1' : '0'; 202 | const imageOptions = theme === 'light' ? imageLightOptions : imageDarkOptions; 203 | const url = new URL(window.location.origin); 204 | url.pathname = `${encodeURIComponent(text)}.${fileType}`; 205 | url.searchParams.append('theme', theme); 206 | url.searchParams.append('md', mdValue); 207 | url.searchParams.append('fontSize', fontSize); 208 | for (let image of images) { 209 | url.searchParams.append('images', image); 210 | } 211 | for (let width of widths) { 212 | url.searchParams.append('widths', width); 213 | } 214 | for (let height of heights) { 215 | url.searchParams.append('heights', height); 216 | } 217 | 218 | return H('div', 219 | { className: 'split' }, 220 | H('div', 221 | { className: 'pull-left' }, 222 | H('div', 223 | H(Field, { 224 | label: 'Theme', 225 | input: H(Dropdown, { 226 | options: themeOptions, 227 | value: theme, 228 | onchange: (val: Theme) => { 229 | const options = val === 'light' ? imageLightOptions : imageDarkOptions 230 | let clone = [...images]; 231 | clone[0] = options[selectedImageIndex].value; 232 | setLoadingState({ theme: val, images: clone }); 233 | } 234 | }) 235 | }), 236 | H(Field, { 237 | label: 'File Type', 238 | input: H(Dropdown, { 239 | options: fileTypeOptions, 240 | value: fileType, 241 | onchange: (val: FileType) => setLoadingState({ fileType: val }) 242 | }) 243 | }), 244 | H(Field, { 245 | label: 'Font Size', 246 | input: H(Dropdown, { 247 | options: fontSizeOptions, 248 | value: fontSize, 249 | onchange: (val: string) => setLoadingState({ fontSize: val }) 250 | }) 251 | }), 252 | H(Field, { 253 | label: 'Text Type', 254 | input: H(Dropdown, { 255 | options: markdownOptions, 256 | value: mdValue, 257 | onchange: (val: string) => setLoadingState({ md: val === '1' }) 258 | }) 259 | }), 260 | H(Field, { 261 | label: 'Text Input', 262 | input: H(TextInput, { 263 | value: text, 264 | oninput: (val: string) => { 265 | console.log('oninput ' + val); 266 | setLoadingState({ text: val, overrideUrl: url }); 267 | } 268 | }) 269 | }), 270 | H(Field, { 271 | label: 'Image 1', 272 | input: H('div', 273 | H(Dropdown, { 274 | options: imageOptions, 275 | value: imageOptions[selectedImageIndex].value, 276 | onchange: (val: string) => { 277 | let clone = [...images]; 278 | clone[0] = val; 279 | const selected = imageOptions.map(o => o.value).indexOf(val); 280 | setLoadingState({ images: clone, selectedImageIndex: selected }); 281 | } 282 | }), 283 | H('div', 284 | { className: 'field-flex' }, 285 | H(TextInput, { 286 | value: widths[0], 287 | type: 'number', 288 | placeholder: 'width', 289 | small: true, 290 | oninput: (val: string) => { 291 | let clone = [...widths]; 292 | clone[0] = val; 293 | setLoadingState({ widths: clone }); 294 | } 295 | }), 296 | H(TextInput, { 297 | value: heights[0], 298 | type: 'number', 299 | placeholder: 'height', 300 | small: true, 301 | oninput: (val: string) => { 302 | let clone = [...heights]; 303 | clone[0] = val; 304 | setLoadingState({ heights: clone }); 305 | } 306 | }) 307 | ) 308 | ), 309 | }), 310 | ...images.slice(1).map((image, i) => H(Field, { 311 | label: `Image ${i + 2}`, 312 | input: H('div', 313 | H(TextInput, { 314 | value: image, 315 | oninput: (val: string) => { 316 | let clone = [...images]; 317 | clone[i + 1] = val; 318 | setLoadingState({ images: clone, overrideUrl: url }); 319 | } 320 | }), 321 | H('div', 322 | { className: 'field-flex' }, 323 | H(TextInput, { 324 | value: widths[i + 1], 325 | type: 'number', 326 | placeholder: 'width', 327 | small: true, 328 | oninput: (val: string) => { 329 | let clone = [...widths]; 330 | clone[i + 1] = val; 331 | setLoadingState({ widths: clone }); 332 | } 333 | }), 334 | H(TextInput, { 335 | value: heights[i + 1], 336 | type: 'number', 337 | placeholder: 'height', 338 | small: true, 339 | oninput: (val: string) => { 340 | let clone = [...heights]; 341 | clone[i + 1] = val; 342 | setLoadingState({ heights: clone }); 343 | } 344 | }) 345 | ), 346 | H('div', 347 | { className: 'field-flex' }, 348 | H(Button, { 349 | label: `Remove Image ${i + 2}`, 350 | onclick: (e: MouseEvent) => { 351 | e.preventDefault(); 352 | const filter = (arr: any[]) => [...arr].filter((_, n) => n !== i + 1); 353 | const imagesClone = filter(images); 354 | const widthsClone = filter(widths); 355 | const heightsClone = filter(heights); 356 | setLoadingState({ images: imagesClone, widths: widthsClone, heights: heightsClone }); 357 | } 358 | }) 359 | ) 360 | ) 361 | })), 362 | H(Field, { 363 | label: `Image ${images.length + 1}`, 364 | input: H(Button, { 365 | label: `Add Image ${images.length + 1}`, 366 | onclick: () => { 367 | const nextImage = images.length === 1 368 | ? 'https://cdn.jsdelivr.net/gh/remojansen/logo.ts@master/ts.svg' 369 | : ''; 370 | setLoadingState({ images: [...images, nextImage] }) 371 | } 372 | }), 373 | }), 374 | ) 375 | ), 376 | H('div', 377 | { className: 'pull-right' }, 378 | H(ImagePreview, { 379 | src: overrideUrl ? overrideUrl.href : url.href, 380 | loading: loading, 381 | onload: () => setState({ loading: false }), 382 | onerror: () => { 383 | setState({ showToast: true, messageToast: 'Oops, an error occurred' }); 384 | setTimeout(() => setState({ showToast: false }), 2000); 385 | }, 386 | onclick: (e: Event) => { 387 | e.preventDefault(); 388 | const success = copee.toClipboard(url.href); 389 | if (success) { 390 | setState({ showToast: true, messageToast: 'Copied image URL to clipboard' }); 391 | setTimeout(() => setState({ showToast: false }), 3000); 392 | } else { 393 | window.open(url.href, '_blank'); 394 | } 395 | return false; 396 | } 397 | }) 398 | ), 399 | H(Toast, { 400 | message: messageToast, 401 | show: showToast, 402 | }) 403 | ); 404 | }; 405 | 406 | R(H(App), document.getElementById('app')); 407 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../api/tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "outDir": "../public/dist" 6 | }, 7 | "include": [ 8 | "./" 9 | ] 10 | } -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/node@*": 6 | version "14.14.28" 7 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.28.tgz#cade4b64f8438f588951a6b35843ce536853f25b" 8 | integrity sha512-lg55ArB+ZiHHbBBttLpzD07akz0QPrZgUODNakeC09i62dnrywr9mFErHuaPlB6I7z+sEbK+IYmplahvplCj2g== 9 | 10 | "@types/puppeteer-core@5.4.0": 11 | version "5.4.0" 12 | resolved "https://registry.yarnpkg.com/@types/puppeteer-core/-/puppeteer-core-5.4.0.tgz#880a7917b4ede95cbfe2d5e81a558cfcb072c0fb" 13 | integrity sha512-yqRPuv4EFcSkTyin6Yy17pN6Qz2vwVwTCJIDYMXbE3j8vTPhv0nCQlZOl5xfi0WHUkqvQsjAR8hAfjeMCoetwg== 14 | dependencies: 15 | "@types/puppeteer" "*" 16 | 17 | "@types/puppeteer@*": 18 | version "5.4.3" 19 | resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-5.4.3.tgz#cdca84aa7751d77448d8a477dbfa0af1f11485f2" 20 | integrity sha512-3nE8YgR9DIsgttLW+eJf6mnXxq8Ge+27m5SU3knWmrlfl6+KOG0Bf9f7Ua7K+C4BnaTMAh3/UpySqdAYvrsvjg== 21 | dependencies: 22 | "@types/node" "*" 23 | 24 | "@types/puppeteer@5.4.4": 25 | version "5.4.4" 26 | resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-5.4.4.tgz#e92abeccc4f46207c3e1b38934a1246be080ccd0" 27 | integrity sha512-3Nau+qi69CN55VwZb0ATtdUAlYlqOOQ3OfQfq0Hqgc4JMFXiQT/XInlwQ9g6LbicDslE6loIFsXFklGh5XmI6Q== 28 | dependencies: 29 | "@types/node" "*" 30 | 31 | "@types/yauzl@^2.9.1": 32 | version "2.9.1" 33 | resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af" 34 | integrity sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA== 35 | dependencies: 36 | "@types/node" "*" 37 | 38 | agent-base@6: 39 | version "6.0.2" 40 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" 41 | integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== 42 | dependencies: 43 | debug "4" 44 | 45 | balanced-match@^1.0.0: 46 | version "1.0.2" 47 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 48 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 49 | 50 | base64-js@^1.3.1: 51 | version "1.5.1" 52 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" 53 | integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== 54 | 55 | bl@^4.0.3: 56 | version "4.1.0" 57 | resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" 58 | integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== 59 | dependencies: 60 | buffer "^5.5.0" 61 | inherits "^2.0.4" 62 | readable-stream "^3.4.0" 63 | 64 | brace-expansion@^1.1.7: 65 | version "1.1.11" 66 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 67 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 68 | dependencies: 69 | balanced-match "^1.0.0" 70 | concat-map "0.0.1" 71 | 72 | buffer-crc32@~0.2.3: 73 | version "0.2.13" 74 | resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" 75 | integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= 76 | 77 | buffer@^5.2.1, buffer@^5.5.0: 78 | version "5.7.1" 79 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" 80 | integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== 81 | dependencies: 82 | base64-js "^1.3.1" 83 | ieee754 "^1.1.13" 84 | 85 | chownr@^1.1.1: 86 | version "1.1.4" 87 | resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" 88 | integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== 89 | 90 | chrome-aws-lambda@7.0.0: 91 | version "7.0.0" 92 | resolved "https://registry.yarnpkg.com/chrome-aws-lambda/-/chrome-aws-lambda-7.0.0.tgz#efb73c8d254d433413cf6866c5eadc752d64b417" 93 | integrity sha512-GbYXRPYtaA0DLfmxQpuZw1SKTP7rOA4UXiATwiFRJ4Ea6PBYSzqK0YjS7B2WimUBe//ybPfNpPIs8A3WpF0xvA== 94 | dependencies: 95 | lambdafs "^2.0.2" 96 | 97 | concat-map@0.0.1: 98 | version "0.0.1" 99 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 100 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 101 | 102 | debug@4, debug@^4.1.1: 103 | version "4.3.1" 104 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" 105 | integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== 106 | dependencies: 107 | ms "2.1.2" 108 | 109 | debug@4.3.2: 110 | version "4.3.2" 111 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" 112 | integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== 113 | dependencies: 114 | ms "2.1.2" 115 | 116 | devtools-protocol@0.0.948846: 117 | version "0.0.948846" 118 | resolved "https://registry.yarnpkg.com/devtools-protocol/-/devtools-protocol-0.0.948846.tgz#bff47e2d1dba060130fa40ed2e5f78b916ba285f" 119 | integrity sha512-5fGyt9xmMqUl2VI7+rnUkKCiAQIpLns8sfQtTENy5L70ktbNw0Z3TFJ1JoFNYdx/jffz4YXU45VF75wKZD7sZQ== 120 | 121 | end-of-stream@^1.1.0, end-of-stream@^1.4.1: 122 | version "1.4.4" 123 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" 124 | integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== 125 | dependencies: 126 | once "^1.4.0" 127 | 128 | extract-zip@2.0.1: 129 | version "2.0.1" 130 | resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" 131 | integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== 132 | dependencies: 133 | debug "^4.1.1" 134 | get-stream "^5.1.0" 135 | yauzl "^2.10.0" 136 | optionalDependencies: 137 | "@types/yauzl" "^2.9.1" 138 | 139 | fd-slicer@~1.1.0: 140 | version "1.1.0" 141 | resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" 142 | integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= 143 | dependencies: 144 | pend "~1.2.0" 145 | 146 | find-up@^4.0.0: 147 | version "4.1.0" 148 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" 149 | integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== 150 | dependencies: 151 | locate-path "^5.0.0" 152 | path-exists "^4.0.0" 153 | 154 | fs-constants@^1.0.0: 155 | version "1.0.0" 156 | resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" 157 | integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== 158 | 159 | fs-extra@^8.0.1: 160 | version "8.1.0" 161 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" 162 | integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== 163 | dependencies: 164 | graceful-fs "^4.2.0" 165 | jsonfile "^4.0.0" 166 | universalify "^0.1.0" 167 | 168 | fs.realpath@^1.0.0: 169 | version "1.0.0" 170 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 171 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 172 | 173 | get-stream@^5.1.0: 174 | version "5.2.0" 175 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" 176 | integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== 177 | dependencies: 178 | pump "^3.0.0" 179 | 180 | glob@^7.1.3: 181 | version "7.1.6" 182 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" 183 | integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== 184 | dependencies: 185 | fs.realpath "^1.0.0" 186 | inflight "^1.0.4" 187 | inherits "2" 188 | minimatch "^3.0.4" 189 | once "^1.3.0" 190 | path-is-absolute "^1.0.0" 191 | 192 | graceful-fs@^4.1.6, graceful-fs@^4.2.0: 193 | version "4.2.6" 194 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" 195 | integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== 196 | 197 | https-proxy-agent@5.0.0: 198 | version "5.0.0" 199 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" 200 | integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== 201 | dependencies: 202 | agent-base "6" 203 | debug "4" 204 | 205 | ieee754@^1.1.13: 206 | version "1.2.1" 207 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" 208 | integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== 209 | 210 | inflight@^1.0.4: 211 | version "1.0.6" 212 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 213 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= 214 | dependencies: 215 | once "^1.3.0" 216 | wrappy "1" 217 | 218 | inherits@2, inherits@^2.0.3, inherits@^2.0.4: 219 | version "2.0.4" 220 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 221 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 222 | 223 | jsonfile@^4.0.0: 224 | version "4.0.0" 225 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" 226 | integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= 227 | optionalDependencies: 228 | graceful-fs "^4.1.6" 229 | 230 | jsonfile@^5.0.0: 231 | version "5.0.0" 232 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-5.0.0.tgz#e6b718f73da420d612823996fdf14a03f6ff6922" 233 | integrity sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w== 234 | dependencies: 235 | universalify "^0.1.2" 236 | optionalDependencies: 237 | graceful-fs "^4.1.6" 238 | 239 | lambdafs@^2.0.2: 240 | version "2.0.2" 241 | resolved "https://registry.yarnpkg.com/lambdafs/-/lambdafs-2.0.2.tgz#6819f27c99891ce3860a38cc6e009073008247e4" 242 | integrity sha512-gr4FQ1eqNZ4k+VmdK1ZzyrcQFv6Hf2MlIgrBq0hXtgMpxU9hG/uKWsNX/FhfugB2aSucJkT4U9NrIVmnVG8NUg== 243 | dependencies: 244 | tar-fs "^2.1.1" 245 | 246 | locate-path@^5.0.0: 247 | version "5.0.0" 248 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" 249 | integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== 250 | dependencies: 251 | p-locate "^4.1.0" 252 | 253 | minimatch@^3.0.4: 254 | version "3.1.2" 255 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 256 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 257 | dependencies: 258 | brace-expansion "^1.1.7" 259 | 260 | mkdirp-classic@^0.5.2: 261 | version "0.5.3" 262 | resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" 263 | integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== 264 | 265 | ms@2.1.2: 266 | version "2.1.2" 267 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 268 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 269 | 270 | node-fetch@2.6.7: 271 | version "2.6.7" 272 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" 273 | integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== 274 | dependencies: 275 | whatwg-url "^5.0.0" 276 | 277 | once@^1.3.0, once@^1.3.1, once@^1.4.0: 278 | version "1.4.0" 279 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 280 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 281 | dependencies: 282 | wrappy "1" 283 | 284 | p-limit@^2.2.0: 285 | version "2.3.0" 286 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" 287 | integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== 288 | dependencies: 289 | p-try "^2.0.0" 290 | 291 | p-locate@^4.1.0: 292 | version "4.1.0" 293 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" 294 | integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== 295 | dependencies: 296 | p-limit "^2.2.0" 297 | 298 | p-try@^2.0.0: 299 | version "2.2.0" 300 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" 301 | integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== 302 | 303 | path-exists@^4.0.0: 304 | version "4.0.0" 305 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 306 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 307 | 308 | path-is-absolute@^1.0.0: 309 | version "1.0.1" 310 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 311 | integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 312 | 313 | pend@~1.2.0: 314 | version "1.2.0" 315 | resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" 316 | integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= 317 | 318 | pkg-dir@4.2.0: 319 | version "4.2.0" 320 | resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" 321 | integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== 322 | dependencies: 323 | find-up "^4.0.0" 324 | 325 | progress@2.0.3: 326 | version "2.0.3" 327 | resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" 328 | integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== 329 | 330 | proxy-from-env@1.1.0: 331 | version "1.1.0" 332 | resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" 333 | integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== 334 | 335 | pump@^3.0.0: 336 | version "3.0.0" 337 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" 338 | integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== 339 | dependencies: 340 | end-of-stream "^1.1.0" 341 | once "^1.3.1" 342 | 343 | puppeteer-core@13.1.2: 344 | version "13.1.2" 345 | resolved "https://registry.yarnpkg.com/puppeteer-core/-/puppeteer-core-13.1.2.tgz#ff4b8f4aacc167b428e3c8df38d23b2121995a04" 346 | integrity sha512-A60/BJkYKpvoWPN0sq0WbOUYey6Wqpn1vlWCt8Ov7PxGIjyuGX2Wb39LObGjOxh4UN+YxCVE+oYQlkIFSmHJtg== 347 | dependencies: 348 | debug "4.3.2" 349 | devtools-protocol "0.0.948846" 350 | extract-zip "2.0.1" 351 | https-proxy-agent "5.0.0" 352 | node-fetch "2.6.7" 353 | pkg-dir "4.2.0" 354 | progress "2.0.3" 355 | proxy-from-env "1.1.0" 356 | rimraf "3.0.2" 357 | tar-fs "2.1.1" 358 | unbzip2-stream "1.4.3" 359 | ws "8.2.3" 360 | 361 | readable-stream@^3.1.1, readable-stream@^3.4.0: 362 | version "3.6.0" 363 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" 364 | integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== 365 | dependencies: 366 | inherits "^2.0.3" 367 | string_decoder "^1.1.1" 368 | util-deprecate "^1.0.1" 369 | 370 | rimraf@3.0.2: 371 | version "3.0.2" 372 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" 373 | integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== 374 | dependencies: 375 | glob "^7.1.3" 376 | 377 | safe-buffer@~5.2.0: 378 | version "5.2.1" 379 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 380 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 381 | 382 | string_decoder@^1.1.1: 383 | version "1.3.0" 384 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" 385 | integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== 386 | dependencies: 387 | safe-buffer "~5.2.0" 388 | 389 | tar-fs@2.1.1, tar-fs@^2.1.1: 390 | version "2.1.1" 391 | resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" 392 | integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== 393 | dependencies: 394 | chownr "^1.1.1" 395 | mkdirp-classic "^0.5.2" 396 | pump "^3.0.0" 397 | tar-stream "^2.1.4" 398 | 399 | tar-stream@^2.1.4: 400 | version "2.2.0" 401 | resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" 402 | integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== 403 | dependencies: 404 | bl "^4.0.3" 405 | end-of-stream "^1.4.1" 406 | fs-constants "^1.0.0" 407 | inherits "^2.0.3" 408 | readable-stream "^3.1.1" 409 | 410 | through@^2.3.8: 411 | version "2.3.8" 412 | resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" 413 | integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= 414 | 415 | tr46@~0.0.3: 416 | version "0.0.3" 417 | resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" 418 | integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= 419 | 420 | twemoji-parser@13.0.0: 421 | version "13.0.0" 422 | resolved "https://registry.yarnpkg.com/twemoji-parser/-/twemoji-parser-13.0.0.tgz#bd9d1b98474f1651dc174696b45cabefdfa405af" 423 | integrity sha512-zMaGdskpH8yKjT2RSE/HwE340R4Fm+fbie4AaqjDa4H/l07YUmAvxkSfNl6awVWNRRQ0zdzLQ8SAJZuY5MgstQ== 424 | 425 | twemoji@13.0.1: 426 | version "13.0.1" 427 | resolved "https://registry.yarnpkg.com/twemoji/-/twemoji-13.0.1.tgz#57ddc8bd86c8175c11376f5f9ab322a02e739c2d" 428 | integrity sha512-mrTBq+XpCLM4zm76NJOjLHoQNV9mHdBt3Cba/T5lS1rxn8ArwpqE47mqTocupNlkvcLxoeZJjYSUW0DU5ZwqZg== 429 | dependencies: 430 | fs-extra "^8.0.1" 431 | jsonfile "^5.0.0" 432 | twemoji-parser "13.0.0" 433 | universalify "^0.1.2" 434 | 435 | typescript@4.1.5: 436 | version "4.1.5" 437 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.5.tgz#123a3b214aaff3be32926f0d8f1f6e704eb89a72" 438 | integrity sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA== 439 | 440 | unbzip2-stream@1.4.3: 441 | version "1.4.3" 442 | resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz#b0da04c4371311df771cdc215e87f2130991ace7" 443 | integrity sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg== 444 | dependencies: 445 | buffer "^5.2.1" 446 | through "^2.3.8" 447 | 448 | universalify@^0.1.0, universalify@^0.1.2: 449 | version "0.1.2" 450 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" 451 | integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== 452 | 453 | util-deprecate@^1.0.1: 454 | version "1.0.2" 455 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 456 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= 457 | 458 | webidl-conversions@^3.0.0: 459 | version "3.0.1" 460 | resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" 461 | integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= 462 | 463 | whatwg-url@^5.0.0: 464 | version "5.0.0" 465 | resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" 466 | integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= 467 | dependencies: 468 | tr46 "~0.0.3" 469 | webidl-conversions "^3.0.0" 470 | 471 | wrappy@1: 472 | version "1.0.2" 473 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 474 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 475 | 476 | ws@8.2.3: 477 | version "8.2.3" 478 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" 479 | integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== 480 | 481 | yauzl@^2.10.0: 482 | version "2.10.0" 483 | resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" 484 | integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= 485 | dependencies: 486 | buffer-crc32 "~0.2.3" 487 | fd-slicer "~1.1.0" 488 | --------------------------------------------------------------------------------