diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..1ec2737 --- /dev/null +++ b/.env.sample @@ -0,0 +1,5 @@ +BASE_URL=https://tailwindui.com + +email=some-email +password=some-password +slowmo=0 diff --git a/README.md b/README.md index 7b4d2f0..614ea05 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,10 @@ The two steps process makes it easy to adjust for future needs. ```sh # Format: - email= password= node tailwindui.js + yarn start # Example: - email='hey@example.com' password='pass123' node tailwindui.js react + yarn start react ``` The command will start chromium and navigate thru tailwindui.com website to copy the components codes. Here's how it looks like: @@ -40,8 +40,8 @@ The two steps process makes it easy to adjust for future needs. 5. Now we can create the mdx files based on above json. To do so, run: ```sh - # Replace `react.json` suffix with `vue.json` as you fit - node tailwindui-storybook.js ./output/tailwindui.react.json react + # Replace "react" with "html" or "vue" (based on step 3) + yarn create-mdx react ``` ![tailwindui-storybook](docs/tailwindui-storybook-mdx.gif) diff --git a/docs/hints/codeblock.png b/docs/hints/codeblock.png new file mode 100644 index 0000000..4143942 Binary files /dev/null and b/docs/hints/codeblock.png differ diff --git a/docs/hints/toggle-code-format.png b/docs/hints/toggle-code-format.png new file mode 100644 index 0000000..5ff0dca Binary files /dev/null and b/docs/hints/toggle-code-format.png differ diff --git a/docs/hints/toggle-code.png b/docs/hints/toggle-code.png new file mode 100644 index 0000000..bae6c9d Binary files /dev/null and b/docs/hints/toggle-code.png differ diff --git a/lib/getComponents.js b/lib/getComponents.js new file mode 100644 index 0000000..21d9d27 --- /dev/null +++ b/lib/getComponents.js @@ -0,0 +1,46 @@ +const baseUrl = process.env.BASE_URL; + +async function getComponents(page, sectionUrl, type = "html") { + // Go to section url. Example: https://tailwindui.com/components/marketing/sections/heroes + await page.goto(sectionUrl); + + const toggleCodeSelector = '[role=tablist] > [id^=headlessui-tabs-tab-]:nth-child(2)' + try { + // Element to toggle code snippet (hint: ../docs/hints/toggle-code.png). + // The element won't exist if the user doesn't have access to the paid component. + await page.waitForSelector(toggleCodeSelector, { timeout: 10 * 1000 }); + } catch (error) { + console.warn(`You do not have access to this section components: ${sectionUrl}`); + } + + const components = await page.evaluate(([toggleCodeSelector, type]) => { + let components = []; + document.querySelectorAll('section[id^="component-"]').forEach((el) => { + const toggleCode = el.querySelector(toggleCodeSelector); + if (!toggleCode) { + return + } + + // Toggle code block + el.querySelector(toggleCodeSelector)?.click(); + + // Change the type of code snippet to HTML/React/Vue (hint: ../docs/hints/toggle-code-format.png) + const toggleSnippetType = el.querySelector('select.form-select') + toggleSnippetType.value = type; + toggleSnippetType.dispatchEvent(new Event('change', { bubbles: true })); + + // Extract code blocks (hint: ../docs/hints/codeblock.png) + const title = el.querySelector("h2 a").innerText; + const codeblock = el.querySelector('pre code')?.innerText; + codeblock && components.push({ title, codeblocks: { [type]: codeblock } }); + }); + return Promise.resolve(components); + }, [toggleCodeSelector, type]); + + // Back to home page + await page.goto(baseUrl); + + return components; +} + +module.exports = getComponents diff --git a/lib/getSections.js b/lib/getSections.js new file mode 100644 index 0000000..3fbf68c --- /dev/null +++ b/lib/getSections.js @@ -0,0 +1,18 @@ +async function getSections(page) { + const sections = await page.evaluate(([]) => { + const buildSections = []; + document + .querySelectorAll('a[href^="/components"]') + .forEach((el) => { + const title = el.children[1]?.innerText; + const countComponents = el.parentElement?.nextElementSibling?.innerText; + const url = el.href; + title && countComponents && buildSections.push({ title, componentsCount: parseInt(countComponents), url }); + }); + return Promise.resolve(buildSections); + }, []); + + return sections; +} + +module.exports = getSections diff --git a/lib/getSections.sample.json b/lib/getSections.sample.json new file mode 100644 index 0000000..dddcc5d --- /dev/null +++ b/lib/getSections.sample.json @@ -0,0 +1,12 @@ +[ + { + "title": "Hero Sections", + "componentsCount": 9, + "url": "https://tailwindui.com/components/marketing/sections/heroes" + }, + { + "title": "Feature Sections", + "componentsCount": 10, + "url": "https://tailwindui.com/components/marketing/sections/feature-sections" + } +] diff --git a/lib/login.js b/lib/login.js new file mode 100644 index 0000000..a946ba9 --- /dev/null +++ b/lib/login.js @@ -0,0 +1,22 @@ +const baseUrl = process.env.BASE_URL; +const selector = { + email: 'input#email', + password: 'input#password', + submit: 'button[type=submit]', +} + +async function login(page, email, password) { + await page.goto(baseUrl + "/login"); + await page.locator(selector.email).fill(email); + await page.locator(selector.password).fill(password); + await page.locator(selector.submit).click(); + + // Assert login succeeded + const loginFailedToken = "These credentials do not match our records"; + const el = await page.$$(`:text("${loginFailedToken}")`); + if (el.length) { + throw new Error("invalid credentials"); + } +} + +module.exports = login diff --git a/package.json b/package.json index eb74250..1613723 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,13 @@ { "name": "tailwindui-storybook", + "engine": ">=14", + "scripts": { + "start": "node -r dotenv/config tailwindui.js", + "create-mdx": "node -r dotenv/config tailwindui-storybook.js" + }, "dependencies": { + "dotenv": "^16.0.3", "lodash": "^4.17.21", - "playwright": "^1.9.2" + "playwright": "^1.27.1" } } diff --git a/tailwindui-storybook.js b/tailwindui-storybook.js index 964be8f..be87bdb 100644 --- a/tailwindui-storybook.js +++ b/tailwindui-storybook.js @@ -2,36 +2,31 @@ const fs = require("fs"); const camelCase = require("lodash/camelCase"); const startCase = require("lodash/startCase"); -const [, , jsonFile, componentType] = process.argv; +const createDir = require("./util/createDir"); -if (!jsonFile || !componentType) { - console.log("usage: tailwind-storybook.js "); - console.log("example: tailwind-storybook.js tailwindui.react.json react"); +const componentType = process.argv[process.argv.length - 1]; +if (!componentType.match(/react|html|vue/)) { + console.log("usage: tailwind-storybook.js "); + console.log("example: tailwind-storybook.js react"); process.exit(0); } -const sections = require(jsonFile); - -const outputDir = `output/tailwindui-${componentType}`; - -const storyFolder = `Tailwind UI`; - -function createDir(dir) { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - console.log("[INFO] directory created:", dir); - } +const jsonFile = `./output/tailwindui.${componentType}.json` +if (!fs.existsSync(jsonFile)) { + console.error("[ERROR] could not find json file at", jsonFile) + console.log(`→ Have you run 'yarn start ${componentType}'?`) + process.exit(1) } -function pascalCase(str) { - return startCase(camelCase(str)).replace(/ /g, ""); -} +const sections = require(jsonFile) +const storyFolder = `Tailwind UI`; +const outputDir = `output/tailwindui-${componentType}`; async function run() { createDir(outputDir); // Uncomment to see data structure - console.log("sections[0]:", sections[0]); + // console.log("sections[0]:", sections[0]); sections.forEach((section) => { const { title, components } = section; @@ -52,12 +47,15 @@ async function run() { components.forEach((component) => { const componentName = pascalCase(component.title); + // Add `Component_` prefix for import name if component name starts with number + const importName = componentName.match(/^\d/) ? `Component_${componentName}` : componentName; + importComponents.push( - `import ${componentName} from "./${componentName}";` + `import ${importName} from "./${componentName}";` ); componentStories.push(` - <${componentName} /> + <${importName} /> `); fs.writeFileSync( @@ -67,16 +65,11 @@ async function run() { }); // Create story file - const storyIndex = ` -import { - Meta, - Story, -} from "@storybook/addon-docs/blocks"; + const storyIndex = `import { Meta, Story } from "@storybook/addon-docs"; ${importComponents.join("\n")} - ${componentStories.join("\n")} `; @@ -84,4 +77,8 @@ ${componentStories.join("\n")} }); } +function pascalCase(str) { + return startCase(camelCase(str)).replace(/ /g, ""); +} + run(); diff --git a/tailwindui.js b/tailwindui.js index 740338b..e2a4602 100644 --- a/tailwindui.js +++ b/tailwindui.js @@ -1,13 +1,23 @@ -const fs = require("fs"); const playwright = require("playwright"); -const baseUrl = "https://tailwindui.com"; +const login = require("./lib/login"); +const getSections = require("./lib/getSections"); +const getComponents = require("./lib/getComponents"); +const createDir = require("./util/createDir"); +const writeToFile = require("./util/writeToFile"); const email = (process.env.email || "").trim(); const password = (process.env.password || "").trim(); +const outputDir = "output"; +const componentsPage = `${process.env.BASE_URL}/components`; + +const guestMode = !email && !password; + // Make sure email and password are provided -if (!email || !password) { +if (guestMode) { + console.log("[INFO] using guest mode because both email and password are not provided") +} else if (!email || !password) { console.log( "[ERROR] please provide email and password of your tailwind ui account as environment variable." ); @@ -17,115 +27,31 @@ if (!email || !password) { process.exit(1); } -const outputDir = "output"; - -async function login(page, email, password) { - await page.goto(baseUrl + "/login"); - await page.fill('[name="email"]', email); - await page.fill('[name="password"]', password); - await page.click('[type="submit"]'); - - // Assert login succeeded - const loginFailedToken = "These credentials do not match our records"; - const el = await page.$$(`:text("${loginFailedToken}")`); - if (el.length) { - throw new Error("invalid credentials"); - } -} - -async function getSections(page) { - const sections = await page.evaluate(([]) => { - let sections = []; - document - .querySelectorAll('#components [href^="/components"]') - .forEach((el) => { - const title = el.querySelector("p:nth-child(1)").innerText; - const components = el.querySelector("p:nth-child(2)").innerText; - const url = el.href; - sections.push({ title, componentsCount: parseInt(components), url }); - }); - return Promise.resolve(sections); - }, []); - - return sections; -} - -async function getComponents(page, sectionUrl, type = "html") { - // Go to section url - await page.goto(sectionUrl); - - // Select react code - await page.waitForSelector('[x-model="activeSnippet"]'); - await page.evaluate( - ([type]) => { - document.querySelectorAll('[x-model="activeSnippet"]').forEach((el) => { - el.value = type; - const evt = document.createEvent("HTMLEvents"); - evt.initEvent("change", false, true); - el.dispatchEvent(evt); - }); - }, - [type] - ); - - // Toggle all codeblocks (otherwise the `innerText` will be empty) - await page.waitForSelector('[x-ref="code"]'); - await page.evaluate(([]) => { - document.querySelectorAll('section[id^="component-"]').forEach((el) => { - el.querySelector('[x-ref="code"]').click(); - }); - }, []); - - // Wait until codeblock is visible - await page.waitForSelector(`[x-ref="codeBlock${type}"]`); - - const components = await page.evaluate( - ([type]) => { - let components = []; - document.querySelectorAll('section[id^="component-"]').forEach((el) => { - const title = el.querySelector("header h2").innerText; - - const component = el.querySelector(`[x-ref="codeBlock${type}"]`) - .innerText; - components.push({ title, codeblocks: { [type]: component } }); - }); - return Promise.resolve(components); - }, - [type] - ); - - // Back to home page - await page.goto(baseUrl); - - return components; -} - async function run() { const [, , componentType] = process.argv; + createDir(outputDir); if (!componentType) { - console.error("[ERROR] please specify component type (html/react/vue)."); + console.error("[ERROR] please specify component type (html|react|vue)."); console.error(`example: node tailwindui.js react`); process.exit(0); } - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir); - console.log("[INFO] output directory created"); - } - const browser = await playwright["chromium"].launch({ - headless: false, - // slowMo: 200, // Uncomment to activate slow mo + headless: !!process.env.headless, + slowMo: process.env.slowmo ? 500 : undefined, }); const ctx = await browser.newContext(); const page = await ctx.newPage(); - console.log("[INFO] logging in to tailwindui.com.."); try { - // Login to tailwindui.com. Throws error if failed. - await login(page, email, password); + // Login to tailwindui.com if not in guest mode. Throws error if failed. + if (!guestMode) { + console.log("[INFO] logging in to tailwindui.com.."); + await login(page, email, password); + } + await page.goto(componentsPage); } catch (e) { const maskPassword = password.replace(/.{4}$/g, "*".repeat(4)); console.log( @@ -135,7 +61,7 @@ async function run() { } console.log(`[INFO] fetching sections..`); - let sections = await getSections(page).catch((e) => { + const sections = await getSections(page).catch((e) => { console.log("[ERROR] getSections failed:", e); }); @@ -150,8 +76,7 @@ async function run() { for (const i in sections) { const { title, componentsCount, url } = sections[i]; console.log( - `[${parseInt(i) + 1}/${ - sections.length + `[${parseInt(i) + 1}/${sections.length }] fetching ${componentType} components: ${title} (${componentsCount} components)` ); @@ -160,6 +85,7 @@ async function run() { components = await getComponents(page, url, componentType); } catch (e) { console.log("[ERROR] getComponent failed:", title, e.message); + writeToFile(outputDir, componentType, sections, true); process.exit(1); } @@ -168,11 +94,7 @@ async function run() { await browser.close(); - const jsonFile = `${outputDir}/tailwindui.${componentType}.json`; - console.log("[INFO] writing json file:", jsonFile); - - fs.writeFileSync(jsonFile, JSON.stringify(sections)); - + writeToFile(outputDir, componentType, sections); console.log("[INFO] done!"); } diff --git a/util/createDir.js b/util/createDir.js new file mode 100644 index 0000000..1c52ee8 --- /dev/null +++ b/util/createDir.js @@ -0,0 +1,10 @@ +const fs = require("fs"); + +function createDir(dir) { + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + console.log("[INFO] directory created:", dir); + } +} + +module.exports = createDir; diff --git a/util/writeToFile.js b/util/writeToFile.js new file mode 100644 index 0000000..368a5a5 --- /dev/null +++ b/util/writeToFile.js @@ -0,0 +1,10 @@ +const fs = require("fs"); + +function writeToFile(outputDir, componentType, sections, error = false) { + const jsonFile = error ? `${outputDir}/incomplete-tailwindui.${componentType}.json` : `${outputDir}/tailwindui.${componentType}.json`; + console.log("[INFO] writing json file:", jsonFile); + + fs.writeFileSync(jsonFile, JSON.stringify(sections)); +} + +module.exports = writeToFile diff --git a/yarn.lock b/yarn.lock index 294813e..11db71c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,273 +2,24 @@ # yarn lockfile v1 -"@types/node@*": - version "14.14.35" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.35.tgz#42c953a4e2b18ab931f72477e7012172f4ffa313" - integrity sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag== - -"@types/yauzl@^2.9.1": - version "2.9.1" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.1.tgz#d10f69f9f522eef3cf98e30afb684a1e1ec923af" - integrity sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA== - dependencies: - "@types/node" "*" - -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= - -commander@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" - integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -debug@4, debug@^4.1.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" - integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ== - dependencies: - ms "2.1.2" - -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - -extract-zip@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" - integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - -fd-slicer@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" - integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= - dependencies: - pend "~1.2.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - -glob@^7.1.3: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -graceful-fs@^4.2.4: - version "4.2.6" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" - integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== - -https-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" - integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== - dependencies: - agent-base "6" - debug "4" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -jpeg-js@^0.4.2: - version "0.4.3" - resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.3.tgz#6158e09f1983ad773813704be80680550eff977b" - integrity sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q== +dotenv@^16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" + integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -mime@^2.4.6: - version "2.5.2" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" - integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" - integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= - -playwright@^1.9.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.9.2.tgz#294910950b76ec4c3dfed6a1e9d28e9b8560b0f4" - integrity sha512-Hsgfk3GZO+hgewRNW9xl9/tHjdZvVwxTseHagbiNpDf90PXICEh8UHXy/2eykeIXrZFMA6W6petEtRWNPi3gfQ== - dependencies: - commander "^6.1.0" - debug "^4.1.1" - extract-zip "^2.0.1" - https-proxy-agent "^5.0.0" - jpeg-js "^0.4.2" - mime "^2.4.6" - pngjs "^5.0.0" - progress "^2.0.3" - proper-lockfile "^4.1.1" - proxy-from-env "^1.1.0" - rimraf "^3.0.2" - stack-utils "^2.0.3" - ws "^7.3.1" - -pngjs@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" - integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== - -progress@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -proper-lockfile@^4.1.1: - version "4.1.2" - resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" - integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== - dependencies: - graceful-fs "^4.2.4" - retry "^0.12.0" - signal-exit "^3.0.2" - -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -stack-utils@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.3.tgz#cd5f030126ff116b78ccb3c027fe302713b61277" - integrity sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw== - dependencies: - escape-string-regexp "^2.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -ws@^7.3.1: - version "7.4.4" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59" - integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw== +playwright-core@1.27.1: + version "1.27.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.27.1.tgz#840ef662e55a3ed759d8b5d3d00a5f885a7184f4" + integrity sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q== -yauzl@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" - integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= +playwright@^1.27.1: + version "1.27.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.27.1.tgz#4eecac5899566c589d4220ca8acc16abe8a67450" + integrity sha512-xXYZ7m36yTtC+oFgqH0eTgullGztKSRMb4yuwLPl8IYSmgBM88QiB+3IWb1mRIC9/NNwcgbG0RwtFlg+EAFQHQ== dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" + playwright-core "1.27.1"