From d4e9b2027e8312d22797f0db9c95d5f2cb66219c Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Fri, 22 Nov 2024 00:42:48 +0100 Subject: [PATCH] astro first commit --- .gitattributes | 3 + .gitignore | 24 + .vscode/extensions.json | 4 + .vscode/launch.json | 11 + README.md | 68 + astro.config.mjs | 15 + package.json | 25 + pnpm-lock.yaml | 4557 +++++++++++++++++ public/favicon.svg | 9 + readingTime.js | 10 + src/components/AboutImage.astro | 30 + src/components/BaseHead.astro | 46 + src/components/FormattedDate.astro | 17 + src/components/Nav.astro | 127 + src/components/PostAttributes.astro | 39 + src/components/PostList.astro | 37 + src/components/PostPreview.astro | 76 + src/components/Signature.astro | 30 + src/components/SkillBar.astro | 39 + src/components/SpacedLetters.astro | 38 + src/components/Tag.astro | 33 + src/components/Tags.astro | 36 + src/consts.ts | 5 + .../blog/4-useful-typescript-tricks.md | 232 + ...ains-tipsntricks-i-wish-id-known-sooner.md | 76 + ...to-directus-for-gatsby-or-sapper-as-cms.md | 266 + ...a-practical-introduction-to-react-hooks.md | 302 ++ ...nsuming-graphql-endpoints-in-typescript.md | 199 + .../automate-github-releases-with-drone.md | 137 + ...-mongodb-inside-of-docker-the-right-way.md | 67 + .../blog/be-your-own-tiny-image-cdn.md | 98 + ...wnloaded-google-photos-takeout-archives.md | 79 + .../blog/create-a-qr-code-for-google-drive.md | 82 + .../blog/going-beyond-npm-meet-yarn-pnpm.md | 96 + ...oid-killing-your-macbook-laptop-battery.md | 171 + ...to-bring-your-neural-network-to-the-web.md | 340 ++ src/content/blog/how-to-search-in-the-jam.md | 222 + .../images/Battery-Charge-Voltage-vs-Time.png | 3 + src/content/blog/images/DST-cycles-web2.jpg | 3 + .../blog/images/IMG_0160-1024x436.jpeg | 3 + src/content/blog/images/IMG_0160.jpeg | 3 + src/content/blog/images/IMG_1709.jpeg | 3 + src/content/blog/images/IMG_1710.jpeg | 3 + src/content/blog/images/IMG_1710.png | 3 + .../blog/images/IMG_1711-653x1024.jpeg | 3 + src/content/blog/images/IMG_1711.jpeg | 3 + .../Screenshot-2020-01-29-at-14.55.28.png | 3 + .../Screenshot-2020-01-29-at-14.57.05.png | 3 + .../Screenshot-2020-04-11-at-23.25.48.png | 3 + .../Screenshot-2021-01-28-at-12.12.59.png | 3 + .../Screenshot-2021-01-28-at-12.14.03.png | 3 + .../Screenshot-2021-03-23-at-10.20.32.png | 3 + ...shot-2021-03-23-at-10.58.31-1-1024x325.png | 3 + .../Screenshot-2021-03-23-at-10.58.31-1.png | 3 + .../Screenshot-2021-03-23-at-10.58.31.png | 3 + ...lessandra-caretto-cAY9X4rPG3g-unsplash.jpg | 3 + ...ina-grubnyak-1254785-unsplash-1024x683.jpg | 3 + ...alina-grubnyak-1254785-unsplash-scaled.jpg | 3 + ...loureiro-BVyNlchWqzs-unsplash-1024x685.jpg | 3 + ...r-loureiro-BVyNlchWqzs-unsplash-scaled.jpg | 3 + ...seny-togulev-1513013-unsplash-1024x576.jpg | 3 + ...arseny-togulev-1513013-unsplash-scaled.jpg | 3 + .../asoggetti-418839-unsplash-1024x684.jpg | 3 + .../asoggetti-418839-unsplash-scaled.jpg | 3 + .../auth-sequence-auth-code-pkce-1024x833.png | 3 + .../images/auth-sequence-auth-code-pkce.png | 3 + ...rett-jordan-qUp3bejuzs-unsplash-scaled.jpg | 3 + src/content/blog/images/cards-1024x567.jpg | 3 + src/content/blog/images/cards-scaled.jpg | 3 + ...s-barbalis-Vvvl-mbboKk-unsplash-scaled.jpg | 3 + ...-robbins-Ru09fQONJWo-unsplash-1024x683.jpg | 3 + ...on-robbins-Ru09fQONJWo-unsplash-scaled.jpg | 3 + ...le-franchi-g2fJ7d7eKSM-unsplash-scaled.jpg | 3 + .../daniele-franchi-g2fJ7d7eKSM-unsplash.jpg | 3 + src/content/blog/images/data.gif | 3 + ...davisco-5E5N49RWtbA-unsplash-3240x2160.jpg | 3 + .../davisco-5E5N49RWtbA-unsplash-scaled-1.jpg | 3 + ...ranck-v-U3sOwViXhkY-unsplash-2880x2160.jpg | 3 + ...franck-v-U3sOwViXhkY-unsplash-scaled-1.jpg | 3 + ...llaume-bolduc-259596-unsplash-1024x741.jpg | 3 + ...uillaume-bolduc-259596-unsplash-scaled.jpg | 3 + src/content/blog/images/howto-1.jpg | 3 + src/content/blog/images/howto.jpg | 3 + .../hyeryi-sVk8nrCQ06g-unsplash-1024x683.jpg | 3 + .../hyeryi-sVk8nrCQ06g-unsplash-scaled.jpg | 3 + ...el-palacio-ImcUkZ72oUs-unsplash-scaled.jpg | 3 + ...on-abdilla-jZWmw6007EY-unsplash-scaled.jpg | 3 + ...zq0ZQ-unsplash-e1562783699383-1024x484.jpg | 3 + ...pxzq0ZQ-unsplash-e1562783699383-scaled.jpg | 3 + ...an-chng-HgoKvtKpyHA-unsplash-3504x2160.jpg | 3 + ...han-chng-HgoKvtKpyHA-unsplash-scaled-1.jpg | 3 + ...-chesser-JKUTrJ4vK00-unsplash-1024x683.jpg | 3 + ...ke-chesser-JKUTrJ4vK00-unsplash-scaled.jpg | 3 + ...-autumns-Ssr26I0QWVY-unsplash-1024x683.jpg | 3 + ...rk-autumns-Ssr26I0QWVY-unsplash-scaled.jpg | 3 + ...s-spiske-8CWoXxaqGrs-unsplash-1024x683.jpg | 3 + ...kus-spiske-8CWoXxaqGrs-unsplash-scaled.jpg | 3 + .../matt-artz-353210-unsplash-1024x780.jpg | 3 + .../matt-artz-353210-unsplash-scaled.jpg | 3 + ...rsience-QGnm_F_nd1E-unsplash1-1024x683.jpg | 3 + ...meagan-carsience-QGnm_F_nd1E-unsplash1.jpg | 3 + .../milkovi-kYlYwQze5vI-unsplash-1-scaled.jpg | 3 + ...connell-byp5TTxUbL0-unsplash-2880x2160.jpg | 3 + ...-connell-byp5TTxUbL0-unsplash-scaled-1.jpg | 3 + .../noah-silliman-doBrZnp_wqA-unsplash.jpg | 3 + ...-nolbert-xe-ss5Tg2mo-unsplash-1024x740.jpg | 3 + .../pawel-nolbert-xe-ss5Tg2mo-unsplash.jpg | 3 + src/content/blog/images/permissions.gif | 3 + ...yan-almuslem-1302778-unsplash-1024x683.jpg | 3 + ...rayan-almuslem-1302778-unsplash-scaled.jpg | 3 + src/content/blog/images/register-bot.jpg | 3 + ...gunasekara-GK8x_XCcDZg-unsplash-scaled.jpg | 3 + ...hindra-gunasekara-GK8x_XCcDZg-unsplash.jpg | 3 + src/content/blog/images/status.jpg | 3 + ...-kelley-t20pc32VbrU-unsplash-3240x2160.jpg | 3 + ...s-kelley-t20pc32VbrU-unsplash-scaled-1.jpg | 3 + ...-fischer-PkbZahEG2Ng-unsplash-1024x497.jpg | 3 + ...as-fischer-PkbZahEG2Ng-unsplash-scaled.jpg | 3 + ...oberanes-gCeH4z9m7bg-unsplash-991x1024.jpg | 3 + .../uriel-soberanes-gCeH4z9m7bg-unsplash.jpg | 3 + ...ud-from-heaven-to-the-depths-of-seafile.md | 161 + src/content/blog/matomo-vs-ublock-origin.md | 185 + ...itor-your-self-hosted-services-for-free.md | 166 + ...ing-made-simple-easily-reduce-bundle-js.md | 120 + ...ce-docker-compose-files-with-yaml-magic.md | 53 + src/content/blog/rust-in-python-made-easy.md | 223 + ...up-your-docker-builds-with-dockerignore.md | 74 + .../blog/step-up-oauth-security-with-pkce.md | 79 + ...ting-detecting-dark-mode-in-the-browser.md | 149 + .../blog/tales-of-learning-go-from-ts.md | 144 + src/content/blog/telegram-bots-are-easy.md | 319 ++ ...-security-checklist-for-modern-websites.md | 226 + .../blog/the-powerful-es6-proxy-object.md | 117 + ...traefik-and-regexp-to-bypass-adblockers.md | 87 + ...mes-i-feel-we-shoot-ourself-in-the-foot.md | 36 + ...-the-next-big-thing-a-reacts-lover-view.md | 192 + ...ss-browser-extensions-without-the-tears.md | 211 + ...rite-your-own-drone-plugin-from-scratch.md | 169 + src/content/config.ts | 26 + src/content/images/about.webp | 3 + src/content/images/home.png | 3 + src/content/page/about.mdx | 63 + src/content/page/privacy.md | 28 + src/env.d.ts | 1 + src/layouts/BlogPost.astro | 36 + src/layouts/PageWithTitle.astro | 52 + src/layouts/Root.astro | 34 + src/pages/[...page].astro | 19 + src/pages/blog/[...slug].astro | 19 + src/pages/blog/index.astro | 11 + src/pages/index.astro | 90 + src/pages/rss.xml.js | 16 + src/pages/tag/[...tag].astro | 28 + src/styles/global.css | 86 + src/styles/preflight.css | 462 ++ tsconfig.json | 6 + 156 files changed, 11589 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/launch.json create mode 100644 README.md create mode 100644 astro.config.mjs create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 public/favicon.svg create mode 100644 readingTime.js create mode 100644 src/components/AboutImage.astro create mode 100644 src/components/BaseHead.astro create mode 100644 src/components/FormattedDate.astro create mode 100644 src/components/Nav.astro create mode 100644 src/components/PostAttributes.astro create mode 100644 src/components/PostList.astro create mode 100644 src/components/PostPreview.astro create mode 100644 src/components/Signature.astro create mode 100644 src/components/SkillBar.astro create mode 100644 src/components/SpacedLetters.astro create mode 100644 src/components/Tag.astro create mode 100644 src/components/Tags.astro create mode 100644 src/consts.ts create mode 100644 src/content/blog/4-useful-typescript-tricks.md create mode 100644 src/content/blog/5-jetbrains-tipsntricks-i-wish-id-known-sooner.md create mode 100644 src/content/blog/a-guide-to-directus-for-gatsby-or-sapper-as-cms.md create mode 100644 src/content/blog/a-practical-introduction-to-react-hooks.md create mode 100644 src/content/blog/a-sane-and-efficient-guide-for-consuming-graphql-endpoints-in-typescript.md create mode 100644 src/content/blog/automate-github-releases-with-drone.md create mode 100644 src/content/blog/backup-mongodb-inside-of-docker-the-right-way.md create mode 100644 src/content/blog/be-your-own-tiny-image-cdn.md create mode 100644 src/content/blog/cleanup-downloaded-google-photos-takeout-archives.md create mode 100644 src/content/blog/create-a-qr-code-for-google-drive.md create mode 100644 src/content/blog/going-beyond-npm-meet-yarn-pnpm.md create mode 100644 src/content/blog/how-to-avoid-killing-your-macbook-laptop-battery.md create mode 100644 src/content/blog/how-to-bring-your-neural-network-to-the-web.md create mode 100644 src/content/blog/how-to-search-in-the-jam.md create mode 100644 src/content/blog/images/Battery-Charge-Voltage-vs-Time.png create mode 100644 src/content/blog/images/DST-cycles-web2.jpg create mode 100644 src/content/blog/images/IMG_0160-1024x436.jpeg create mode 100644 src/content/blog/images/IMG_0160.jpeg create mode 100644 src/content/blog/images/IMG_1709.jpeg create mode 100644 src/content/blog/images/IMG_1710.jpeg create mode 100644 src/content/blog/images/IMG_1710.png create mode 100644 src/content/blog/images/IMG_1711-653x1024.jpeg create mode 100644 src/content/blog/images/IMG_1711.jpeg create mode 100644 src/content/blog/images/Screenshot-2020-01-29-at-14.55.28.png create mode 100644 src/content/blog/images/Screenshot-2020-01-29-at-14.57.05.png create mode 100644 src/content/blog/images/Screenshot-2020-04-11-at-23.25.48.png create mode 100644 src/content/blog/images/Screenshot-2021-01-28-at-12.12.59.png create mode 100644 src/content/blog/images/Screenshot-2021-01-28-at-12.14.03.png create mode 100644 src/content/blog/images/Screenshot-2021-03-23-at-10.20.32.png create mode 100644 src/content/blog/images/Screenshot-2021-03-23-at-10.58.31-1-1024x325.png create mode 100644 src/content/blog/images/Screenshot-2021-03-23-at-10.58.31-1.png create mode 100644 src/content/blog/images/Screenshot-2021-03-23-at-10.58.31.png create mode 100644 src/content/blog/images/alessandra-caretto-cAY9X4rPG3g-unsplash.jpg create mode 100644 src/content/blog/images/alina-grubnyak-1254785-unsplash-1024x683.jpg create mode 100644 src/content/blog/images/alina-grubnyak-1254785-unsplash-scaled.jpg create mode 100644 src/content/blog/images/amador-loureiro-BVyNlchWqzs-unsplash-1024x685.jpg create mode 100644 src/content/blog/images/amador-loureiro-BVyNlchWqzs-unsplash-scaled.jpg create mode 100644 src/content/blog/images/arseny-togulev-1513013-unsplash-1024x576.jpg create mode 100644 src/content/blog/images/arseny-togulev-1513013-unsplash-scaled.jpg create mode 100644 src/content/blog/images/asoggetti-418839-unsplash-1024x684.jpg create mode 100644 src/content/blog/images/asoggetti-418839-unsplash-scaled.jpg create mode 100644 src/content/blog/images/auth-sequence-auth-code-pkce-1024x833.png create mode 100644 src/content/blog/images/auth-sequence-auth-code-pkce.png create mode 100644 src/content/blog/images/brett-jordan-qUp3bejuzs-unsplash-scaled.jpg create mode 100644 src/content/blog/images/cards-1024x567.jpg create mode 100644 src/content/blog/images/cards-scaled.jpg create mode 100644 src/content/blog/images/chris-barbalis-Vvvl-mbboKk-unsplash-scaled.jpg create mode 100644 src/content/blog/images/clayton-robbins-Ru09fQONJWo-unsplash-1024x683.jpg create mode 100644 src/content/blog/images/clayton-robbins-Ru09fQONJWo-unsplash-scaled.jpg create mode 100644 src/content/blog/images/daniele-franchi-g2fJ7d7eKSM-unsplash-scaled.jpg create mode 100644 src/content/blog/images/daniele-franchi-g2fJ7d7eKSM-unsplash.jpg create mode 100644 src/content/blog/images/data.gif create mode 100644 src/content/blog/images/davisco-5E5N49RWtbA-unsplash-3240x2160.jpg create mode 100644 src/content/blog/images/davisco-5E5N49RWtbA-unsplash-scaled-1.jpg create mode 100644 src/content/blog/images/franck-v-U3sOwViXhkY-unsplash-2880x2160.jpg create mode 100644 src/content/blog/images/franck-v-U3sOwViXhkY-unsplash-scaled-1.jpg create mode 100644 src/content/blog/images/guillaume-bolduc-259596-unsplash-1024x741.jpg create mode 100644 src/content/blog/images/guillaume-bolduc-259596-unsplash-scaled.jpg create mode 100644 src/content/blog/images/howto-1.jpg create mode 100644 src/content/blog/images/howto.jpg create mode 100644 src/content/blog/images/hyeryi-sVk8nrCQ06g-unsplash-1024x683.jpg create mode 100644 src/content/blog/images/hyeryi-sVk8nrCQ06g-unsplash-scaled.jpg create mode 100644 src/content/blog/images/israel-palacio-ImcUkZ72oUs-unsplash-scaled.jpg create mode 100644 src/content/blog/images/jason-abdilla-jZWmw6007EY-unsplash-scaled.jpg create mode 100644 src/content/blog/images/jielin-chen-pKQIpxzq0ZQ-unsplash-e1562783699383-1024x484.jpg create mode 100644 src/content/blog/images/jielin-chen-pKQIpxzq0ZQ-unsplash-e1562783699383-scaled.jpg create mode 100644 src/content/blog/images/jonathan-chng-HgoKvtKpyHA-unsplash-3504x2160.jpg create mode 100644 src/content/blog/images/jonathan-chng-HgoKvtKpyHA-unsplash-scaled-1.jpg create mode 100644 src/content/blog/images/luke-chesser-JKUTrJ4vK00-unsplash-1024x683.jpg create mode 100644 src/content/blog/images/luke-chesser-JKUTrJ4vK00-unsplash-scaled.jpg create mode 100644 src/content/blog/images/mark-autumns-Ssr26I0QWVY-unsplash-1024x683.jpg create mode 100644 src/content/blog/images/mark-autumns-Ssr26I0QWVY-unsplash-scaled.jpg create mode 100644 src/content/blog/images/markus-spiske-8CWoXxaqGrs-unsplash-1024x683.jpg create mode 100644 src/content/blog/images/markus-spiske-8CWoXxaqGrs-unsplash-scaled.jpg create mode 100644 src/content/blog/images/matt-artz-353210-unsplash-1024x780.jpg create mode 100644 src/content/blog/images/matt-artz-353210-unsplash-scaled.jpg create mode 100644 src/content/blog/images/meagan-carsience-QGnm_F_nd1E-unsplash1-1024x683.jpg create mode 100644 src/content/blog/images/meagan-carsience-QGnm_F_nd1E-unsplash1.jpg create mode 100644 src/content/blog/images/milkovi-kYlYwQze5vI-unsplash-1-scaled.jpg create mode 100644 src/content/blog/images/natasha-connell-byp5TTxUbL0-unsplash-2880x2160.jpg create mode 100644 src/content/blog/images/natasha-connell-byp5TTxUbL0-unsplash-scaled-1.jpg create mode 100644 src/content/blog/images/noah-silliman-doBrZnp_wqA-unsplash.jpg create mode 100644 src/content/blog/images/pawel-nolbert-xe-ss5Tg2mo-unsplash-1024x740.jpg create mode 100644 src/content/blog/images/pawel-nolbert-xe-ss5Tg2mo-unsplash.jpg create mode 100644 src/content/blog/images/permissions.gif create mode 100644 src/content/blog/images/rayan-almuslem-1302778-unsplash-1024x683.jpg create mode 100644 src/content/blog/images/rayan-almuslem-1302778-unsplash-scaled.jpg create mode 100644 src/content/blog/images/register-bot.jpg create mode 100644 src/content/blog/images/ruchindra-gunasekara-GK8x_XCcDZg-unsplash-scaled.jpg create mode 100644 src/content/blog/images/ruchindra-gunasekara-GK8x_XCcDZg-unsplash.jpg create mode 100644 src/content/blog/images/status.jpg create mode 100644 src/content/blog/images/thomas-kelley-t20pc32VbrU-unsplash-3240x2160.jpg create mode 100644 src/content/blog/images/thomas-kelley-t20pc32VbrU-unsplash-scaled-1.jpg create mode 100644 src/content/blog/images/tobias-fischer-PkbZahEG2Ng-unsplash-1024x497.jpg create mode 100644 src/content/blog/images/tobias-fischer-PkbZahEG2Ng-unsplash-scaled.jpg create mode 100644 src/content/blog/images/uriel-soberanes-gCeH4z9m7bg-unsplash-991x1024.jpg create mode 100644 src/content/blog/images/uriel-soberanes-gCeH4z9m7bg-unsplash.jpg create mode 100644 src/content/blog/leaving-nextcloud-from-heaven-to-the-depths-of-seafile.md create mode 100644 src/content/blog/matomo-vs-ublock-origin.md create mode 100644 src/content/blog/monitor-your-self-hosted-services-for-free.md create mode 100644 src/content/blog/react-code-splitting-made-simple-easily-reduce-bundle-js.md create mode 100644 src/content/blog/reduce-docker-compose-files-with-yaml-magic.md create mode 100644 src/content/blog/rust-in-python-made-easy.md create mode 100644 src/content/blog/speed-up-your-docker-builds-with-dockerignore.md create mode 100644 src/content/blog/step-up-oauth-security-with-pkce.md create mode 100644 src/content/blog/supporting-detecting-dark-mode-in-the-browser.md create mode 100644 src/content/blog/tales-of-learning-go-from-ts.md create mode 100644 src/content/blog/telegram-bots-are-easy.md create mode 100644 src/content/blog/the-essential-no-excuses-security-checklist-for-modern-websites.md create mode 100644 src/content/blog/the-powerful-es6-proxy-object.md create mode 100644 src/content/blog/use-traefik-and-regexp-to-bypass-adblockers.md create mode 100644 src/content/blog/why-i-love-js-but-sometimes-i-feel-we-shoot-ourself-in-the-foot.md create mode 100644 src/content/blog/why-i-think-svelte-is-the-next-big-thing-a-reacts-lover-view.md create mode 100644 src/content/blog/write-cross-browser-extensions-without-the-tears.md create mode 100644 src/content/blog/write-your-own-drone-plugin-from-scratch.md create mode 100644 src/content/config.ts create mode 100644 src/content/images/about.webp create mode 100644 src/content/images/home.png create mode 100644 src/content/page/about.mdx create mode 100644 src/content/page/privacy.md create mode 100644 src/env.d.ts create mode 100644 src/layouts/BlogPost.astro create mode 100644 src/layouts/PageWithTitle.astro create mode 100644 src/layouts/Root.astro create mode 100644 src/pages/[...page].astro create mode 100644 src/pages/blog/[...slug].astro create mode 100644 src/pages/blog/index.astro create mode 100644 src/pages/index.astro create mode 100644 src/pages/rss.xml.js create mode 100644 src/pages/tag/[...tag].astro create mode 100644 src/styles/global.css create mode 100644 src/styles/preflight.css create mode 100644 tsconfig.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3ba4998 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +src/images/* filter=lfs diff=lfs merge=lfs -text +*.afphoto filter=lfs diff=lfs merge=lfs -text +*.afdesign filter=lfs diff=lfs merge=lfs -text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16d54bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# build output +dist/ +# generated types +.astro/ + +# dependencies +node_modules/ + +# logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + + +# environment variables +.env +.env.production + +# macOS-specific files +.DS_Store + +# jetbrains setting folder +.idea/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..56f043d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + "recommendations": ["astro-build.astro-vscode", "unifiedjs.vscode-mdx"], + "unwantedRecommendations": [] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d642209 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "command": "./node_modules/.bin/astro dev", + "name": "Development server", + "request": "launch", + "type": "node-terminal" + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..758716e --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Astro Starter Kit: Blog + +```sh +npm create astro@latest -- --template blog +``` + +[![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/astro/tree/latest/examples/blog) +[![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/astro/tree/latest/examples/blog) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/withastro/astro?devcontainer_path=.devcontainer/blog/devcontainer.json) + +> πŸ§‘β€πŸš€ **Seasoned astronaut?** Delete this file. Have fun! + +![blog](https://github.com/withastro/astro/assets/2244813/ff10799f-a816-4703-b967-c78997e8323d) + +Features: + +- βœ… Minimal styling (make it your own!) +- βœ… 100/100 Lighthouse performance +- βœ… SEO-friendly with canonical URLs and OpenGraph data +- βœ… Sitemap support +- βœ… RSS Feed support +- βœ… Markdown & MDX support + +## πŸš€ Project Structure + +Inside of your Astro project, you'll see the following folders and files: + +```text +β”œβ”€β”€ public/ +β”œβ”€β”€ src/ +β”‚Β Β  β”œβ”€β”€ components/ +β”‚Β Β  β”œβ”€β”€ content/ +β”‚Β Β  β”œβ”€β”€ layouts/ +β”‚Β Β  └── pages/ +β”œβ”€β”€ astro.config.mjs +β”œβ”€β”€ README.md +β”œβ”€β”€ package.json +└── tsconfig.json +``` + +Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name. + +There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components. + +The `src/content/` directory contains "collections" of related Markdown and MDX documents. Use `getCollection()` to retrieve posts from `src/content/blog/`, and type-check your frontmatter using an optional schema. See [Astro's Content Collections docs](https://docs.astro.build/en/guides/content-collections/) to learn more. + +Any static assets, like images, can be placed in the `public/` directory. + +## 🧞 Commands + +All commands are run from the root of the project, from a terminal: + +| Command | Action | +| :------------------------ | :----------------------------------------------- | +| `npm install` | Installs dependencies | +| `npm run dev` | Starts local dev server at `localhost:4321` | +| `npm run build` | Build your production site to `./dist/` | +| `npm run preview` | Preview your build locally, before deploying | +| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | +| `npm run astro -- --help` | Get help using the Astro CLI | + +## πŸ‘€ Want to learn more? + +Check out [our documentation](https://docs.astro.build) or jump into our [Discord server](https://astro.build/chat). + +## Credit + +This theme is based off of the lovely [Bear Blog](https://github.com/HermanMartinus/bearblog/). diff --git a/astro.config.mjs b/astro.config.mjs new file mode 100644 index 0000000..489a48a --- /dev/null +++ b/astro.config.mjs @@ -0,0 +1,15 @@ +// @ts-check +import { defineConfig } from 'astro/config' +import mdx from '@astrojs/mdx' + +import sitemap from '@astrojs/sitemap' +import { remarkReadingTime } from './readingTime' + +// https://astro.build/config +export default defineConfig({ + site: 'https://example.com', + integrations: [mdx(), sitemap()], + markdown: { + remarkPlugins: [remarkReadingTime], + }, +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..f3ebdad --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "private": true, + "type": "module", + "scripts": { + "astro": "astro", + "build": "astro check && astro build", + "dev": "astro dev", + "preview": "astro preview", + "start": "astro dev" + }, + "dependencies": { + "@astrojs/check": "^0.9.4", + "@astrojs/mdx": "^3.1.9", + "@astrojs/rss": "^4.0.9", + "@astrojs/sitemap": "^3.2.1", + "@fontsource-variable/jost": "^5.1.1", + "@fontsource-variable/playfair-display": "^5.1.0", + "astro": "^4.16.13", + "mdast-util-to-string": "^4.0.0", + "reading-time": "^1.5.0", + "sharp": "^0.33.5", + "typescript": "^5.6.3" + }, + "packageManager": "pnpm@9.12.3" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..903e8bb --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,4557 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@astrojs/check': + specifier: ^0.9.4 + version: 0.9.4(typescript@5.6.3) + '@astrojs/mdx': + specifier: ^3.1.9 + version: 3.1.9(astro@4.16.13(rollup@4.27.3)(typescript@5.6.3)) + '@astrojs/rss': + specifier: ^4.0.9 + version: 4.0.9 + '@astrojs/sitemap': + specifier: ^3.2.1 + version: 3.2.1 + '@fontsource-variable/jost': + specifier: ^5.1.1 + version: 5.1.1 + '@fontsource-variable/playfair-display': + specifier: ^5.1.0 + version: 5.1.0 + astro: + specifier: ^4.16.13 + version: 4.16.13(rollup@4.27.3)(typescript@5.6.3) + mdast-util-to-string: + specifier: ^4.0.0 + version: 4.0.0 + reading-time: + specifier: ^1.5.0 + version: 1.5.0 + sharp: + specifier: ^0.33.5 + version: 0.33.5 + typescript: + specifier: ^5.6.3 + version: 5.6.3 + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@astrojs/check@0.9.4': + resolution: {integrity: sha512-IOheHwCtpUfvogHHsvu0AbeRZEnjJg3MopdLddkJE70mULItS/Vh37BHcI00mcOJcH1vhD3odbpvWokpxam7xA==} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + + '@astrojs/compiler@2.10.3': + resolution: {integrity: sha512-bL/O7YBxsFt55YHU021oL+xz+B/9HvGNId3F9xURN16aeqDK9juHGktdkCSXz+U4nqFACq6ZFvWomOzhV+zfPw==} + + '@astrojs/internal-helpers@0.4.1': + resolution: {integrity: sha512-bMf9jFihO8YP940uD70SI/RDzIhUHJAolWVcO1v5PUivxGKvfLZTLTVVxEYzGYyPsA3ivdLNqMnL5VgmQySa+g==} + + '@astrojs/language-server@2.15.4': + resolution: {integrity: sha512-JivzASqTPR2bao9BWsSc/woPHH7OGSGc9aMxXL4U6egVTqBycB3ZHdBJPuOCVtcGLrzdWTosAqVPz1BVoxE0+A==} + hasBin: true + peerDependencies: + prettier: ^3.0.0 + prettier-plugin-astro: '>=0.11.0' + peerDependenciesMeta: + prettier: + optional: true + prettier-plugin-astro: + optional: true + + '@astrojs/markdown-remark@5.3.0': + resolution: {integrity: sha512-r0Ikqr0e6ozPb5bvhup1qdWnSPUvQu6tub4ZLYaKyG50BXZ0ej6FhGz3GpChKpH7kglRFPObJd/bDyf2VM9pkg==} + + '@astrojs/mdx@3.1.9': + resolution: {integrity: sha512-3jPD4Bff6lIA20RQoonnZkRtZ9T3i0HFm6fcDF7BMsKIZ+xBP2KXzQWiuGu62lrVCmU612N+SQVGl5e0fI+zWg==} + engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0} + peerDependencies: + astro: ^4.8.0 + + '@astrojs/prism@3.1.0': + resolution: {integrity: sha512-Z9IYjuXSArkAUx3N6xj6+Bnvx8OdUSHA8YoOgyepp3+zJmtVYJIl/I18GozdJVW1p5u/CNpl3Km7/gwTJK85cw==} + engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0} + + '@astrojs/rss@4.0.9': + resolution: {integrity: sha512-W1qeLc/WP1vMS5xXa+BnaLU0paeSeGjN8RJVAoBaOIkQuKXjIUA9hvPno89heo73in5i67g40gy70oeeHMqp6A==} + + '@astrojs/sitemap@3.2.1': + resolution: {integrity: sha512-uxMfO8f7pALq0ADL6Lk68UV6dNYjJ2xGUzyjjVj60JLBs5a6smtlkBYv3tQ0DzoqwS7c9n4FUx5lgv0yPo/fgA==} + + '@astrojs/telemetry@3.1.0': + resolution: {integrity: sha512-/ca/+D8MIKEC8/A9cSaPUqQNZm+Es/ZinRv0ZAzvu2ios7POQSsVD+VOj7/hypWNsNM3T7RpfgNq7H2TU1KEHA==} + engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0} + + '@astrojs/yaml2ts@0.2.2': + resolution: {integrity: sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ==} + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.26.2': + resolution: {integrity: sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.26.0': + resolution: {integrity: sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.2': + resolution: {integrity: sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.25.9': + resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.25.9': + resolution: {integrity: sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.25.9': + resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.26.0': + resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.25.9': + resolution: {integrity: sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.25.9': + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.25.9': + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.25.9': + resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.26.0': + resolution: {integrity: sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.26.2': + resolution: {integrity: sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.25.9': + resolution: {integrity: sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.25.9': + resolution: {integrity: sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.25.9': + resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.25.9': + resolution: {integrity: sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.0': + resolution: {integrity: sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==} + engines: {node: '>=6.9.0'} + + '@emmetio/abbreviation@2.3.3': + resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} + + '@emmetio/css-abbreviation@2.1.8': + resolution: {integrity: sha512-s9yjhJ6saOO/uk1V74eifykk2CBYi01STTK3WlXWGOepyKa23ymJ053+DNQjpFcy1ingpaO7AxCcwLvHFY9tuw==} + + '@emmetio/css-parser@0.4.0': + resolution: {integrity: sha512-z7wkxRSZgrQHXVzObGkXG+Vmj3uRlpM11oCZ9pbaz0nFejvCDmAiNDpY75+wgXOcffKpj4rzGtwGaZxfJKsJxw==} + + '@emmetio/html-matcher@1.3.0': + resolution: {integrity: sha512-NTbsvppE5eVyBMuyGfVu2CRrLvo7J4YHb6t9sBFLyY03WYhXET37qA4zOYUjBWFCRHO7pS1B9khERtY0f5JXPQ==} + + '@emmetio/scanner@1.0.4': + resolution: {integrity: sha512-IqRuJtQff7YHHBk4G8YZ45uB9BaAGcwQeVzgj/zj8/UdOhtQpEIupUhSk8dys6spFIWVZVeK20CzGEnqR5SbqA==} + + '@emmetio/stream-reader-utils@0.1.0': + resolution: {integrity: sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A==} + + '@emmetio/stream-reader@2.2.0': + resolution: {integrity: sha512-fXVXEyFA5Yv3M3n8sUGT7+fvecGrZP4k6FnWWMSZVQf69kAq0LLpaBQLGcPR30m3zMmKYhECP4k/ZkzvhEW5kw==} + + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@fontsource-variable/jost@5.1.1': + resolution: {integrity: sha512-AjyHFqOjU9OKqqniTmptvBK6Z9QAKBcX84yrpglqnKsY5lUrFLZUQE+KKPnFCYa6nIfJtSzW9TaPjazQvIxtCA==} + + '@fontsource-variable/playfair-display@5.1.0': + resolution: {integrity: sha512-51UJAHznXiGkchEOT5AYFuIy9N9m1JE7YxcdOtY0H4SKgzK9YYGKpeCXv3dz2e1gAd+BP2TyR0QXNnVjgeCbgw==} + + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@mdx-js/mdx@3.1.0': + resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@oslojs/encoding@1.1.0': + resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} + + '@rollup/pluginutils@5.1.3': + resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.27.3': + resolution: {integrity: sha512-EzxVSkIvCFxUd4Mgm4xR9YXrcp976qVaHnqom/Tgm+vU79k4vV4eYTjmRvGfeoW8m9LVcsAy/lGjcgVegKEhLQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.27.3': + resolution: {integrity: sha512-LJc5pDf1wjlt9o/Giaw9Ofl+k/vLUaYsE2zeQGH85giX2F+wn/Cg8b3c5CDP3qmVmeO5NzwVUzQQxwZvC2eQKw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.27.3': + resolution: {integrity: sha512-OuRysZ1Mt7wpWJ+aYKblVbJWtVn3Cy52h8nLuNSzTqSesYw1EuN6wKp5NW/4eSre3mp12gqFRXOKTcN3AI3LqA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.27.3': + resolution: {integrity: sha512-xW//zjJMlJs2sOrCmXdB4d0uiilZsOdlGQIC/jjmMWT47lkLLoB1nsNhPUcnoqyi5YR6I4h+FjBpILxbEy8JRg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.27.3': + resolution: {integrity: sha512-58E0tIcwZ+12nK1WiLzHOD8I0d0kdrY/+o7yFVPRHuVGY3twBwzwDdTIBGRxLmyjciMYl1B/U515GJy+yn46qw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.27.3': + resolution: {integrity: sha512-78fohrpcVwTLxg1ZzBMlwEimoAJmY6B+5TsyAZ3Vok7YabRBUvjYTsRXPTjGEvv/mfgVBepbW28OlMEz4w8wGA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.27.3': + resolution: {integrity: sha512-h2Ay79YFXyQi+QZKo3ISZDyKaVD7uUvukEHTOft7kh00WF9mxAaxZsNs3o/eukbeKuH35jBvQqrT61fzKfAB/Q==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.27.3': + resolution: {integrity: sha512-Sv2GWmrJfRY57urktVLQ0VKZjNZGogVtASAgosDZ1aUB+ykPxSi3X1nWORL5Jk0sTIIwQiPH7iE3BMi9zGWfkg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.27.3': + resolution: {integrity: sha512-FPoJBLsPW2bDNWjSrwNuTPUt30VnfM8GPGRoLCYKZpPx0xiIEdFip3dH6CqgoT0RnoGXptaNziM0WlKgBc+OWQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.27.3': + resolution: {integrity: sha512-TKxiOvBorYq4sUpA0JT+Fkh+l+G9DScnG5Dqx7wiiqVMiRSkzTclP35pE6eQQYjP4Gc8yEkJGea6rz4qyWhp3g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.27.3': + resolution: {integrity: sha512-v2M/mPvVUKVOKITa0oCFksnQQ/TqGrT+yD0184/cWHIu0LoIuYHwox0Pm3ccXEz8cEQDLk6FPKd1CCm+PlsISw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.27.3': + resolution: {integrity: sha512-LdrI4Yocb1a/tFVkzmOE5WyYRgEBOyEhWYJe4gsDWDiwnjYKjNs7PS6SGlTDB7maOHF4kxevsuNBl2iOcj3b4A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.27.3': + resolution: {integrity: sha512-d4wVu6SXij/jyiwPvI6C4KxdGzuZOvJ6y9VfrcleHTwo68fl8vZC5ZYHsCVPUi4tndCfMlFniWgwonQ5CUpQcA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.27.3': + resolution: {integrity: sha512-/6bn6pp1fsCGEY5n3yajmzZQAh+mW4QPItbiWxs69zskBzJuheb3tNynEjL+mKOsUSFK11X4LYF2BwwXnzWleA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.27.3': + resolution: {integrity: sha512-nBXOfJds8OzUT1qUreT/en3eyOXd2EH5b0wr2bVB5999qHdGKkzGzIyKYaKj02lXk6wpN71ltLIaQpu58YFBoQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.27.3': + resolution: {integrity: sha512-ogfbEVQgIZOz5WPWXF2HVb6En+kWzScuxJo/WdQTqEgeyGkaa2ui5sQav9Zkr7bnNCLK48uxmmK0TySm22eiuw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.27.3': + resolution: {integrity: sha512-ecE36ZBMLINqiTtSNQ1vzWc5pXLQHlf/oqGp/bSbi7iedcjcNb6QbCBNG73Euyy2C+l/fn8qKWEwxr+0SSfs3w==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.27.3': + resolution: {integrity: sha512-vliZLrDmYKyaUoMzEbMTg2JkerfBjn03KmAw9CykO0Zzkzoyd7o3iZNam/TpyWNjNT+Cz2iO3P9Smv2wgrR+Eg==} + cpu: [x64] + os: [win32] + + '@shikijs/core@1.23.1': + resolution: {integrity: sha512-NuOVgwcHgVC6jBVH5V7iblziw6iQbWWHrj5IlZI3Fqu2yx9awH7OIQkXIcsHsUmY19ckwSgUMgrqExEyP5A0TA==} + + '@shikijs/engine-javascript@1.23.1': + resolution: {integrity: sha512-i/LdEwT5k3FVu07SiApRFwRcSJs5QM9+tod5vYCPig1Ywi8GR30zcujbxGQFJHwYD7A5BUqagi8o5KS+LEVgBg==} + + '@shikijs/engine-oniguruma@1.23.1': + resolution: {integrity: sha512-KQ+lgeJJ5m2ISbUZudLR1qHeH3MnSs2mjFg7bnencgs5jDVPeJ2NVDJ3N5ZHbcTsOIh0qIueyAJnwg7lg7kwXQ==} + + '@shikijs/types@1.23.1': + resolution: {integrity: sha512-98A5hGyEhzzAgQh2dAeHKrWW4HfCMeoFER2z16p5eJ+vmPeF6lZ/elEne6/UCU551F/WqkopqRsr1l2Yu6+A0g==} + + '@shikijs/vscode-textmate@9.3.0': + resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==} + + '@types/acorn@4.0.6': + resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.6.8': + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.6': + resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/ms@0.7.34': + resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + + '@types/node@17.0.45': + resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} + + '@types/sax@1.2.7': + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@ungap/structured-clone@1.2.0': + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + + '@volar/kit@2.4.10': + resolution: {integrity: sha512-ul+rLeO9RlFDgkY/FhPWMnpFqAsjvjkKz8VZeOY5YCJMwTblmmSBlNJtFNxSBx9t/k1q80nEthLyxiJ50ZbIAg==} + peerDependencies: + typescript: '*' + + '@volar/language-core@2.4.10': + resolution: {integrity: sha512-hG3Z13+nJmGaT+fnQzAkS0hjJRa2FCeqZt6Bd+oGNhUkQ+mTFsDETg5rqUTxyzIh5pSOGY7FHCWUS8G82AzLCA==} + + '@volar/language-server@2.4.10': + resolution: {integrity: sha512-odQsgrJh8hOXfxkSj/BSnpjThb2/KDhbxZnG/XAEx6E3QGDQv4hAOz9GWuKoNs0tkjgwphQGIwDMT1JYaTgRJw==} + + '@volar/language-service@2.4.10': + resolution: {integrity: sha512-VxUiWS11rnRzakkqw5x1LPhsz+RBfD0CrrFarLGW2/voliYXEdCuSOM3r8JyNRvMvP4uwhD38ccAdTcULQEAIQ==} + + '@volar/source-map@2.4.10': + resolution: {integrity: sha512-OCV+b5ihV0RF3A7vEvNyHPi4G4kFa6ukPmyVocmqm5QzOd8r5yAtiNvaPEjl8dNvgC/lj4JPryeeHLdXd62rWA==} + + '@volar/typescript@2.4.10': + resolution: {integrity: sha512-F8ZtBMhSXyYKuBfGpYwqA5rsONnOwAVvjyE7KPYJ7wgZqo2roASqNWUnianOomJX5u1cxeRooHV59N0PhvEOgw==} + + '@vscode/emmet-helper@2.10.0': + resolution: {integrity: sha512-UHw1EQRgLbSYkyB73/7wR/IzV6zTBnbzEHuuU4Z6b95HKf2lmeTdGwBIwspWBSRrnIA1TI2x2tetBym6ErA7Gw==} + + '@vscode/l10n@0.0.18': + resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + + astro@4.16.13: + resolution: {integrity: sha512-Mtd76+BC0zLWqoXpf9xc731AhdH4MNh5JFHYdLRvSH0Nqn48hA64dPGh/cWsJvh/DZFmC0NTZusM1Qq2gyNaVg==} + engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} + hasBin: true + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + base-64@1.0.0: + resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + + boxen@8.0.1: + resolution: {integrity: sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==} + engines: {node: '>=18'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.24.2: + resolution: {integrity: sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + camelcase@8.0.0: + resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} + engines: {node: '>=16'} + + caniuse-lite@1.0.30001680: + resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + chokidar@4.0.1: + resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} + engines: {node: '>= 14.16.0'} + + ci-info@4.1.0: + resolution: {integrity: sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==} + engines: {node: '>=8'} + + cli-boxes@3.0.0: + resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} + engines: {node: '>=10'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + common-ancestor-path@1.0.1: + resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.0.2: + resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + deterministic-object-hash@2.0.2: + resolution: {integrity: sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==} + engines: {node: '>=18'} + + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + diff@5.2.0: + resolution: {integrity: sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==} + engines: {node: '>=0.3.1'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dset@3.1.4: + resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} + engines: {node: '>=4'} + + electron-to-chromium@1.5.63: + resolution: {integrity: sha512-ddeXKuY9BHo/mw145axlyWjlJ1UBt4WK3AlvkT7W2AbqfRQoacVoRUCF6wL3uIx/8wT9oLKXzI+rFqHHscByaA==} + + emmet@2.4.11: + resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} + + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + + emoji-regex@10.4.0: + resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-module-lexer@1.5.4: + resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-uri@3.0.3: + resolution: {integrity: sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==} + + fast-xml-parser@4.5.0: + resolution: {integrity: sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==} + hasBin: true + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up-simple@1.0.0: + resolution: {integrity: sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==} + engines: {node: '>=18'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-yarn-workspace-root2@1.2.16: + resolution: {integrity: sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==} + + flattie@1.1.1: + resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==} + engines: {node: '>=8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.3.0: + resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} + engines: {node: '>=18'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + hast-util-from-html@2.0.3: + resolution: {integrity: sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==} + + hast-util-from-parse5@8.0.2: + resolution: {integrity: sha512-SfMzfdAi/zAoZ1KkFEyyeXBn7u/ShQrfd675ZEE9M3qj+PMFX05xubzRyF76CCSJu8au9jgVxDV1+okFvgZU4A==} + + hast-util-is-element@3.0.0: + resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-estree@3.1.0: + resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==} + + hast-util-to-html@9.0.3: + resolution: {integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==} + + hast-util-to-jsx-runtime@2.3.2: + resolution: {integrity: sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==} + + hast-util-to-parse5@8.0.0: + resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==} + + hast-util-to-text@4.0.2: + resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.0: + resolution: {integrity: sha512-jzaLBGavEDKHrc5EfFImKN7nZKKBdSLIdGvCwDZ9TfzbF2ffXiov8CKE445L2Z1Ek2t/m4SKQ2j6Ipv7NyUolw==} + + html-escaper@3.0.3: + resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + inline-style-parser@0.1.1: + resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} + + inline-style-parser@0.2.4: + resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@2.3.1: + resolution: {integrity: sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + load-yaml-file@0.2.0: + resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} + engines: {node: '>=6'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.13: + resolution: {integrity: sha512-8rYBO+MsWkgjDSOvLomYnzhdwEG51olQ4zL5KXnNJWV5MNmrb4rTZdrtkhxjnD/QyZUqR/Z/XDsUs/4ej2nx0g==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + + mdast-util-find-and-replace@3.0.1: + resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.0.0: + resolution: {integrity: sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.0.0: + resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.1.3: + resolution: {integrity: sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromark-core-commonmark@2.0.2: + resolution: {integrity: sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.0: + resolution: {integrity: sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-mdx-expression@3.0.0: + resolution: {integrity: sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==} + + micromark-extension-mdx-jsx@3.0.1: + resolution: {integrity: sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-mdx-expression@2.0.2: + resolution: {integrity: sha512-5E5I2pFzJyg2CtemqAbcyCktpHXuJbABnsb32wX2U8IQKhhVFBqkcZR5LRm1WVoFqa4kTueZK4abep7wdo9nrw==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-events-to-acorn@2.0.2: + resolution: {integrity: sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.0.3: + resolution: {integrity: sha512-VXJJuNxYWSoYL6AJ6OQECCFGhIU2GGHMw8tahogePBrjkG8aCCas3ibkp7RnVOSTClg2is05/R7maAhF1XyQMg==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.1: + resolution: {integrity: sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==} + + micromark@4.0.1: + resolution: {integrity: sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + neotraverse@0.6.18: + resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} + engines: {node: '>= 10'} + + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + + node-releases@2.0.18: + resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + oniguruma-to-es@0.4.1: + resolution: {integrity: sha512-rNcEohFz095QKGRovP/yqPIKc+nP+Sjs4YTHMv33nMePGKrq/r2eu9Yh4646M5XluGJsUnmwoXuiXE69KDs+fQ==} + + ora@8.1.1: + resolution: {integrity: sha512-YWielGi1XzG1UTvOaCFaNgEnuhZVMSHYkW/FQ7UX8O26PtlpdM84c0f7wLPlkvx2RfiQmnzd61d/MGxmpQeJPw==} + engines: {node: '>=18'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@6.1.0: + resolution: {integrity: sha512-H0jc0q1vOzlEk0TqAKXKZxdl7kX3OFUzCnNVUnq5Pc3DGo0kpeaMuPqxQn235HibwBEb0/pm9dgKTjXy66fBkg==} + engines: {node: '>=18'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-queue@8.0.1: + resolution: {integrity: sha512-NXzu9aQJTAzbBqOt2hwsR63ea7yvxJc0PwN/zobNAudYfb1B7R08SzB4TsLeSbUCuG467NhnoT0oO6w1qRO+BA==} + engines: {node: '>=18'} + + p-timeout@6.1.3: + resolution: {integrity: sha512-UJUyfKbwvr/uZSV6btANfb+0t/mOhKV/KXcCUTp8FcQI+v/0d+wXqH4htrW0E4rR6WiEO/EPvUFiV9D5OI4vlw==} + engines: {node: '>=14.16'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parse-entities@4.0.1: + resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} + + parse-latin@7.0.0: + resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + + parse5@7.2.1: + resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pify@4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + + preferred-pm@4.0.0: + resolution: {integrity: sha512-gYBeFTZLu055D8Vv3cSPox/0iTPtkzxpLroSYYA7WXgRi31WCJ51Uyl8ZiPeUUjyvs2MBzK+S8v9JVUgHU/Sqw==} + engines: {node: '>=18.12'} + + prettier@2.8.7: + resolution: {integrity: sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==} + engines: {node: '>=10.13.0'} + hasBin: true + + prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + + prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + + property-information@6.5.0: + resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readdirp@4.0.2: + resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} + engines: {node: '>= 14.16.0'} + + reading-time@1.5.0: + resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} + + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.0: + resolution: {integrity: sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==} + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + + regex-recursion@4.2.1: + resolution: {integrity: sha512-QHNZyZAeKdndD1G3bKAbBEKOSSK4KOHQrAJ01N1LJeb0SoH4DJIeFhp0uUpETgONifS4+P3sOgoA1dhzgrQvhA==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@5.0.2: + resolution: {integrity: sha512-/pczGbKIQgfTMRV0XjABvc5RzLqQmwqxLHdQao2RTXPk+pmTXB2P0IaUHYdYyk412YLwUIkaeMd5T+RzVgTqnQ==} + + rehype-parse@9.0.1: + resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + + rehype-stringify@10.0.1: + resolution: {integrity: sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==} + + rehype@13.0.2: + resolution: {integrity: sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==} + + remark-gfm@4.0.0: + resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==} + + remark-mdx@3.1.0: + resolution: {integrity: sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.1: + resolution: {integrity: sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==} + + remark-smartypants@3.0.2: + resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==} + engines: {node: '>=16.0.0'} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + request-light@0.5.8: + resolution: {integrity: sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg==} + + request-light@0.7.0: + resolution: {integrity: sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + retext-latin@4.0.0: + resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} + + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + + retext-stringify@4.0.0: + resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} + + retext@9.0.0: + resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.27.3: + resolution: {integrity: sha512-SLsCOnlmGt9VoZ9Ek8yBK8tAdmPHeppkw+Xa7yDlCEhDTvwYei03JlWo1fdc7YTfLZ4tD8riJCUyAgTbszk1fQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shiki@1.23.1: + resolution: {integrity: sha512-8kxV9TH4pXgdKGxNOkrSMydn1Xf6It8lsle0fiqxf7a1149K1WGtdOu3Zb91T5r1JpvRPxqxU3C2XdZZXQnrig==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + sitemap@8.0.0: + resolution: {integrity: sha512-+AbdxhM9kJsHtruUF39bwS/B0Fytw6Fr1o4ZAIAEqA6cke2xcoO2GleBw9Zw7nRzILVEgz7zBM5GiTJjie1G9A==} + engines: {node: '>=14.0.0', npm: '>=6.0.0'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + + stream-replace-string@2.0.0: + resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + + style-to-object@0.4.4: + resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} + + style-to-object@1.0.8: + resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + + tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + tsconfck@3.1.4: + resolution: {integrity: sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-fest@4.27.0: + resolution: {integrity: sha512-3IMSWgP7C5KSQqmo1wjhKrwsvXAtF33jO3QY+Uy++ia7hqvgSK6iXbbg5PbDBc1P2ZbNEDgejOrN4YooXvhwCw==} + engines: {node: '>=16'} + + typesafe-path@0.2.2: + resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==} + + typescript-auto-import-cache@0.3.5: + resolution: {integrity: sha512-fAIveQKsoYj55CozUiBoj4b/7WpN0i4o74wiGY5JVUEoD0XiqDk1tJqTEjgzL2/AizKQrXxyRosSebyDzBZKjw==} + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-find-after@5.0.0: + resolution: {integrity: sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-modify-children@4.0.0: + resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-children@3.0.0: + resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + update-browserslist-db@1.1.1: + resolution: {integrity: sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@5.4.11: + resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitefu@1.0.3: + resolution: {integrity: sha512-iKKfOMBHob2WxEJbqbJjHAkmYgvFDPhuqrO82om83S8RLk+17FtyMBfcyeH8GqD0ihShtkMW/zzJgiA51hCNCQ==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + + volar-service-css@0.0.62: + resolution: {integrity: sha512-JwNyKsH3F8PuzZYuqPf+2e+4CTU8YoyUHEHVnoXNlrLe7wy9U3biomZ56llN69Ris7TTy/+DEX41yVxQpM4qvg==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-emmet@0.0.62: + resolution: {integrity: sha512-U4dxWDBWz7Pi4plpbXf4J4Z/ss6kBO3TYrACxWNsE29abu75QzVS0paxDDhI6bhqpbDFXlpsDhZ9aXVFpnfGRQ==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-html@0.0.62: + resolution: {integrity: sha512-Zw01aJsZRh4GTGUjveyfEzEqpULQUdQH79KNEiKVYHZyuGtdBRYCHlrus1sueSNMxwwkuF5WnOHfvBzafs8yyQ==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-prettier@0.0.62: + resolution: {integrity: sha512-h2yk1RqRTE+vkYZaI9KYuwpDfOQRrTEMvoHol0yW4GFKc75wWQRrb5n/5abDrzMPrkQbSip8JH2AXbvrRtYh4w==} + peerDependencies: + '@volar/language-service': ~2.4.0 + prettier: ^2.2 || ^3.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + prettier: + optional: true + + volar-service-typescript-twoslash-queries@0.0.62: + resolution: {integrity: sha512-KxFt4zydyJYYI0kFAcWPTh4u0Ha36TASPZkAnNY784GtgajerUqM80nX/W1d0wVhmcOFfAxkVsf/Ed+tiYU7ng==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-typescript@0.0.62: + resolution: {integrity: sha512-p7MPi71q7KOsH0eAbZwPBiKPp9B2+qrdHAd6VY5oTo9BUXatsOAdakTm9Yf0DUj6uWBAaOT01BSeVOPwucMV1g==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + volar-service-yaml@0.0.62: + resolution: {integrity: sha512-k7gvv7sk3wa+nGll3MaSKyjwQsJjIGCHFjVkl3wjaSP2nouKyn9aokGmqjrl39mi88Oy49giog2GkZH526wjig==} + peerDependencies: + '@volar/language-service': ~2.4.0 + peerDependenciesMeta: + '@volar/language-service': + optional: true + + vscode-css-languageservice@6.3.1: + resolution: {integrity: sha512-1BzTBuJfwMc3A0uX4JBdJgoxp74cjj4q2mDJdp49yD/GuAq4X0k5WtK6fNcMYr+FfJ9nqgR6lpfCSZDkARJ5qQ==} + + vscode-html-languageservice@5.3.1: + resolution: {integrity: sha512-ysUh4hFeW/WOWz/TO9gm08xigiSsV/FOAZ+DolgJfeLftna54YdmZ4A+lIn46RbdO3/Qv5QHTn1ZGqmrXQhZyA==} + + vscode-json-languageservice@4.1.8: + resolution: {integrity: sha512-0vSpg6Xd9hfV+eZAaYN63xVVMOTmJ4GgHxXnkLCh+9RsQBkWKIghzLhW2B9ebfG+LQQg8uLtsQ2aUKjTgE+QOg==} + engines: {npm: '>=7.0.0'} + + vscode-jsonrpc@6.0.0: + resolution: {integrity: sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==} + engines: {node: '>=8.0.0 || >=10.0.0'} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.16.0: + resolution: {integrity: sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.16.0: + resolution: {integrity: sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@7.0.0: + resolution: {integrity: sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==} + hasBin: true + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-nls@5.2.0: + resolution: {integrity: sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng==} + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + which-pm-runs@1.1.0: + resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==} + engines: {node: '>=4'} + + which-pm@3.0.0: + resolution: {integrity: sha512-ysVYmw6+ZBhx3+ZkcPwRuJi38ZOTLJJ33PSHaitLxSKUMsh0LkKd0nC69zZCwt5D+AYUcMK2hhw4yWny20vSGg==} + engines: {node: '>=18.12'} + + widest-line@5.0.0: + resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==} + engines: {node: '>=18'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@9.0.0: + resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==} + engines: {node: '>=18'} + + xxhash-wasm@1.1.0: + resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml-language-server@1.15.0: + resolution: {integrity: sha512-N47AqBDCMQmh6mBLmI6oqxryHRzi33aPFPsJhYy3VTUGCdLHYjGh4FZzpUjRlphaADBBkDmnkM/++KNIOHi5Rw==} + hasBin: true + + yaml@2.2.2: + resolution: {integrity: sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==} + engines: {node: '>= 14'} + + yaml@2.6.1: + resolution: {integrity: sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==} + engines: {node: '>= 14'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yocto-queue@1.1.1: + resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} + engines: {node: '>=12.20'} + + zod-to-json-schema@3.23.5: + resolution: {integrity: sha512-5wlSS0bXfF/BrL4jPAbz9da5hDlDptdEppYfe+x4eIJ7jioqKG9uUxOwPzqof09u/XeVdrgFu29lZi+8XNDJtA==} + peerDependencies: + zod: ^3.23.3 + + zod-to-ts@1.2.0: + resolution: {integrity: sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==} + peerDependencies: + typescript: ^4.9.4 || ^5.0.2 + zod: ^3 + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@astrojs/check@0.9.4(typescript@5.6.3)': + dependencies: + '@astrojs/language-server': 2.15.4(typescript@5.6.3) + chokidar: 4.0.1 + kleur: 4.1.5 + typescript: 5.6.3 + yargs: 17.7.2 + transitivePeerDependencies: + - prettier + - prettier-plugin-astro + + '@astrojs/compiler@2.10.3': {} + + '@astrojs/internal-helpers@0.4.1': {} + + '@astrojs/language-server@2.15.4(typescript@5.6.3)': + dependencies: + '@astrojs/compiler': 2.10.3 + '@astrojs/yaml2ts': 0.2.2 + '@jridgewell/sourcemap-codec': 1.5.0 + '@volar/kit': 2.4.10(typescript@5.6.3) + '@volar/language-core': 2.4.10 + '@volar/language-server': 2.4.10 + '@volar/language-service': 2.4.10 + fast-glob: 3.3.2 + muggle-string: 0.4.1 + volar-service-css: 0.0.62(@volar/language-service@2.4.10) + volar-service-emmet: 0.0.62(@volar/language-service@2.4.10) + volar-service-html: 0.0.62(@volar/language-service@2.4.10) + volar-service-prettier: 0.0.62(@volar/language-service@2.4.10) + volar-service-typescript: 0.0.62(@volar/language-service@2.4.10) + volar-service-typescript-twoslash-queries: 0.0.62(@volar/language-service@2.4.10) + volar-service-yaml: 0.0.62(@volar/language-service@2.4.10) + vscode-html-languageservice: 5.3.1 + vscode-uri: 3.0.8 + transitivePeerDependencies: + - typescript + + '@astrojs/markdown-remark@5.3.0': + dependencies: + '@astrojs/prism': 3.1.0 + github-slugger: 2.0.0 + hast-util-from-html: 2.0.3 + hast-util-to-text: 4.0.2 + import-meta-resolve: 4.1.0 + mdast-util-definitions: 6.0.0 + rehype-raw: 7.0.0 + rehype-stringify: 10.0.1 + remark-gfm: 4.0.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.1 + remark-smartypants: 3.0.2 + shiki: 1.23.1 + unified: 11.0.5 + unist-util-remove-position: 5.0.0 + unist-util-visit: 5.0.0 + unist-util-visit-parents: 6.0.1 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/mdx@3.1.9(astro@4.16.13(rollup@4.27.3)(typescript@5.6.3))': + dependencies: + '@astrojs/markdown-remark': 5.3.0 + '@mdx-js/mdx': 3.1.0(acorn@8.14.0) + acorn: 8.14.0 + astro: 4.16.13(rollup@4.27.3)(typescript@5.6.3) + es-module-lexer: 1.5.4 + estree-util-visit: 2.0.0 + gray-matter: 4.0.3 + hast-util-to-html: 9.0.3 + kleur: 4.1.5 + rehype-raw: 7.0.0 + remark-gfm: 4.0.0 + remark-smartypants: 3.0.2 + source-map: 0.7.4 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@astrojs/prism@3.1.0': + dependencies: + prismjs: 1.29.0 + + '@astrojs/rss@4.0.9': + dependencies: + fast-xml-parser: 4.5.0 + kleur: 4.1.5 + + '@astrojs/sitemap@3.2.1': + dependencies: + sitemap: 8.0.0 + stream-replace-string: 2.0.0 + zod: 3.23.8 + + '@astrojs/telemetry@3.1.0': + dependencies: + ci-info: 4.1.0 + debug: 4.3.7 + dlv: 1.1.3 + dset: 3.1.4 + is-docker: 3.0.0 + is-wsl: 3.1.0 + which-pm-runs: 1.1.0 + transitivePeerDependencies: + - supports-color + + '@astrojs/yaml2ts@0.2.2': + dependencies: + yaml: 2.6.1 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.26.2': {} + + '@babel/core@7.26.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/helper-compilation-targets': 7.25.9 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.0) + '@babel/helpers': 7.26.0 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + convert-source-map: 2.0.0 + debug: 4.3.7 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.26.2': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.0.2 + + '@babel/helper-annotate-as-pure@7.25.9': + dependencies: + '@babel/types': 7.26.0 + + '@babel/helper-compilation-targets@7.25.9': + dependencies: + '@babel/compat-data': 7.26.2 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-module-imports@7.25.9': + dependencies: + '@babel/traverse': 7.25.9 + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.25.9 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.25.9': {} + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helpers@7.26.0': + dependencies: + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + + '@babel/parser@7.26.2': + dependencies: + '@babel/types': 7.26.0 + + '@babel/plugin-syntax-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-plugin-utils': 7.25.9 + + '@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.0)': + dependencies: + '@babel/core': 7.26.0 + '@babel/helper-annotate-as-pure': 7.25.9 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-plugin-utils': 7.25.9 + '@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/types': 7.26.0 + transitivePeerDependencies: + - supports-color + + '@babel/template@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@babel/traverse@7.25.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.2 + '@babel/parser': 7.26.2 + '@babel/template': 7.25.9 + '@babel/types': 7.26.0 + debug: 4.3.7 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.26.0': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@emmetio/abbreviation@2.3.3': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-abbreviation@2.1.8': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/css-parser@0.4.0': + dependencies: + '@emmetio/stream-reader': 2.2.0 + '@emmetio/stream-reader-utils': 0.1.0 + + '@emmetio/html-matcher@1.3.0': + dependencies: + '@emmetio/scanner': 1.0.4 + + '@emmetio/scanner@1.0.4': {} + + '@emmetio/stream-reader-utils@0.1.0': {} + + '@emmetio/stream-reader@2.2.0': {} + + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@fontsource-variable/jost@5.1.1': {} + + '@fontsource-variable/playfair-display@5.1.0': {} + + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@mdx-js/mdx@3.1.0(acorn@8.14.0)': + dependencies: + '@types/estree': 1.0.6 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdx': 2.0.13 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.2 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.0(acorn@8.14.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.0 + remark-parse: 11.0.0 + remark-rehype: 11.1.1 + source-map: 0.7.4 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + transitivePeerDependencies: + - acorn + - supports-color + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@oslojs/encoding@1.1.0': {} + + '@rollup/pluginutils@5.1.3(rollup@4.27.3)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.27.3 + + '@rollup/rollup-android-arm-eabi@4.27.3': + optional: true + + '@rollup/rollup-android-arm64@4.27.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.27.3': + optional: true + + '@rollup/rollup-darwin-x64@4.27.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.27.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.27.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.27.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.27.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.27.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.27.3': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.27.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.27.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.27.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.27.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.27.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.27.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.27.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.27.3': + optional: true + + '@shikijs/core@1.23.1': + dependencies: + '@shikijs/engine-javascript': 1.23.1 + '@shikijs/engine-oniguruma': 1.23.1 + '@shikijs/types': 1.23.1 + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.3 + + '@shikijs/engine-javascript@1.23.1': + dependencies: + '@shikijs/types': 1.23.1 + '@shikijs/vscode-textmate': 9.3.0 + oniguruma-to-es: 0.4.1 + + '@shikijs/engine-oniguruma@1.23.1': + dependencies: + '@shikijs/types': 1.23.1 + '@shikijs/vscode-textmate': 9.3.0 + + '@shikijs/types@1.23.1': + dependencies: + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@9.3.0': {} + + '@types/acorn@4.0.6': + dependencies: + '@types/estree': 1.0.6 + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.6 + + '@types/babel__generator@7.6.8': + dependencies: + '@babel/types': 7.26.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + + '@types/babel__traverse@7.20.6': + dependencies: + '@babel/types': 7.26.0 + + '@types/cookie@0.6.0': {} + + '@types/debug@4.1.12': + dependencies: + '@types/ms': 0.7.34 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.6 + + '@types/estree@1.0.6': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdx@2.0.13': {} + + '@types/ms@0.7.34': {} + + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + + '@types/node@17.0.45': {} + + '@types/sax@1.2.7': + dependencies: + '@types/node': 17.0.45 + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@ungap/structured-clone@1.2.0': {} + + '@volar/kit@2.4.10(typescript@5.6.3)': + dependencies: + '@volar/language-service': 2.4.10 + '@volar/typescript': 2.4.10 + typesafe-path: 0.2.2 + typescript: 5.6.3 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + '@volar/language-core@2.4.10': + dependencies: + '@volar/source-map': 2.4.10 + + '@volar/language-server@2.4.10': + dependencies: + '@volar/language-core': 2.4.10 + '@volar/language-service': 2.4.10 + '@volar/typescript': 2.4.10 + path-browserify: 1.0.1 + request-light: 0.7.0 + vscode-languageserver: 9.0.1 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + '@volar/language-service@2.4.10': + dependencies: + '@volar/language-core': 2.4.10 + vscode-languageserver-protocol: 3.17.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + '@volar/source-map@2.4.10': {} + + '@volar/typescript@2.4.10': + dependencies: + '@volar/language-core': 2.4.10 + path-browserify: 1.0.1 + vscode-uri: 3.0.8 + + '@vscode/emmet-helper@2.10.0': + dependencies: + emmet: 2.4.11 + jsonc-parser: 2.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + + '@vscode/l10n@0.0.18': {} + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.3 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + arg@5.0.2: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + array-iterate@2.0.1: {} + + astring@1.9.0: {} + + astro@4.16.13(rollup@4.27.3)(typescript@5.6.3): + dependencies: + '@astrojs/compiler': 2.10.3 + '@astrojs/internal-helpers': 0.4.1 + '@astrojs/markdown-remark': 5.3.0 + '@astrojs/telemetry': 3.1.0 + '@babel/core': 7.26.0 + '@babel/plugin-transform-react-jsx': 7.25.9(@babel/core@7.26.0) + '@babel/types': 7.26.0 + '@oslojs/encoding': 1.1.0 + '@rollup/pluginutils': 5.1.3(rollup@4.27.3) + '@types/babel__core': 7.20.5 + '@types/cookie': 0.6.0 + acorn: 8.14.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + boxen: 8.0.1 + ci-info: 4.1.0 + clsx: 2.1.1 + common-ancestor-path: 1.0.1 + cookie: 0.7.2 + cssesc: 3.0.0 + debug: 4.3.7 + deterministic-object-hash: 2.0.2 + devalue: 5.1.1 + diff: 5.2.0 + dlv: 1.1.3 + dset: 3.1.4 + es-module-lexer: 1.5.4 + esbuild: 0.21.5 + estree-walker: 3.0.3 + fast-glob: 3.3.2 + flattie: 1.1.1 + github-slugger: 2.0.0 + gray-matter: 4.0.3 + html-escaper: 3.0.3 + http-cache-semantics: 4.1.1 + js-yaml: 4.1.0 + kleur: 4.1.5 + magic-string: 0.30.13 + magicast: 0.3.5 + micromatch: 4.0.8 + mrmime: 2.0.0 + neotraverse: 0.6.18 + ora: 8.1.1 + p-limit: 6.1.0 + p-queue: 8.0.1 + preferred-pm: 4.0.0 + prompts: 2.4.2 + rehype: 13.0.2 + semver: 7.6.3 + shiki: 1.23.1 + tinyexec: 0.3.1 + tsconfck: 3.1.4(typescript@5.6.3) + unist-util-visit: 5.0.0 + vfile: 6.0.3 + vite: 5.4.11 + vitefu: 1.0.3(vite@5.4.11) + which-pm: 3.0.0 + xxhash-wasm: 1.1.0 + yargs-parser: 21.1.1 + zod: 3.23.8 + zod-to-json-schema: 3.23.5(zod@3.23.8) + zod-to-ts: 1.2.0(typescript@5.6.3)(zod@3.23.8) + optionalDependencies: + sharp: 0.33.5 + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - typescript + + axobject-query@4.1.0: {} + + bail@2.0.2: {} + + base-64@1.0.0: {} + + boxen@8.0.1: + dependencies: + ansi-align: 3.0.1 + camelcase: 8.0.0 + chalk: 5.3.0 + cli-boxes: 3.0.0 + string-width: 7.2.0 + type-fest: 4.27.0 + widest-line: 5.0.0 + wrap-ansi: 9.0.0 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.24.2: + dependencies: + caniuse-lite: 1.0.30001680 + electron-to-chromium: 1.5.63 + node-releases: 2.0.18 + update-browserslist-db: 1.1.1(browserslist@4.24.2) + + camelcase@8.0.0: {} + + caniuse-lite@1.0.30001680: {} + + ccount@2.0.1: {} + + chalk@5.3.0: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + chokidar@4.0.1: + dependencies: + readdirp: 4.0.2 + + ci-info@4.1.0: {} + + cli-boxes@3.0.0: {} + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clsx@2.1.1: {} + + collapse-white-space@2.1.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + comma-separated-tokens@2.0.3: {} + + common-ancestor-path@1.0.1: {} + + convert-source-map@2.0.0: {} + + cookie@0.7.2: {} + + cssesc@3.0.0: {} + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.0.2: + dependencies: + character-entities: 2.0.2 + + dequal@2.0.3: {} + + detect-libc@2.0.3: {} + + deterministic-object-hash@2.0.2: + dependencies: + base-64: 1.0.0 + + devalue@5.1.1: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + diff@5.2.0: {} + + dlv@1.1.3: {} + + dset@3.1.4: {} + + electron-to-chromium@1.5.63: {} + + emmet@2.4.11: + dependencies: + '@emmetio/abbreviation': 2.3.3 + '@emmetio/css-abbreviation': 2.1.8 + + emoji-regex-xs@1.0.0: {} + + emoji-regex@10.4.0: {} + + emoji-regex@8.0.0: {} + + entities@4.5.0: {} + + es-module-lexer@1.5.4: {} + + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.14.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.2 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-string-regexp@5.0.0: {} + + esprima@4.0.1: {} + + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.6 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.6 + devlop: 1.1.0 + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.4 + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.6 + + eventemitter3@5.0.1: {} + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.0.3: {} + + fast-xml-parser@4.5.0: + dependencies: + strnum: 1.0.5 + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up-simple@1.0.0: {} + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-yarn-workspace-root2@1.2.16: + dependencies: + micromatch: 4.0.8 + pkg-dir: 4.2.0 + + flattie@1.1.1: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.3.0: {} + + github-slugger@2.0.0: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + globals@11.12.0: {} + + graceful-fs@4.2.11: {} + + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + + hast-util-from-html@2.0.3: + dependencies: + '@types/hast': 3.0.4 + devlop: 1.1.0 + hast-util-from-parse5: 8.0.2 + parse5: 7.2.1 + vfile: 6.0.3 + vfile-message: 4.0.2 + + hast-util-from-parse5@8.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.0 + property-information: 6.5.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-is-element@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.2.0 + hast-util-from-parse5: 8.0.2 + hast-util-to-parse5: 8.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + parse5: 7.2.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-estree@3.1.0: + dependencies: + '@types/estree': 1.0.6 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + style-to-object: 0.4.4 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + hast-util-to-html@9.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-jsx-runtime@2.3.2: + dependencies: + '@types/estree': 1.0.6 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + style-to-object: 1.0.8 + unist-util-position: 5.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + hast-util-to-parse5@8.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-text@4.0.2: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + hast-util-is-element: 3.0.0 + unist-util-find-after: 5.0.0 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.0: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 6.5.0 + space-separated-tokens: 2.0.2 + + html-escaper@3.0.3: {} + + html-void-elements@3.0.0: {} + + http-cache-semantics@4.1.1: {} + + import-meta-resolve@4.1.0: {} + + inline-style-parser@0.1.1: {} + + inline-style-parser@0.2.4: {} + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-arrayish@0.3.2: {} + + is-decimal@2.0.1: {} + + is-docker@3.0.0: {} + + is-extendable@0.1.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@2.0.1: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-interactive@2.0.0: {} + + is-number@7.0.0: {} + + is-plain-obj@4.1.0: {} + + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + js-tokens@4.0.0: {} + + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.0.2: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonc-parser@2.3.1: {} + + jsonc-parser@3.3.1: {} + + kind-of@6.0.3: {} + + kleur@3.0.3: {} + + kleur@4.1.5: {} + + load-yaml-file@0.2.0: + dependencies: + graceful-fs: 4.2.11 + js-yaml: 3.14.1 + pify: 4.0.1 + strip-bom: 3.0.0 + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + lodash@4.17.21: {} + + log-symbols@6.0.0: + dependencies: + chalk: 5.3.0 + is-unicode-supported: 1.3.0 + + longest-streak@3.1.0: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.13: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + source-map-js: 1.2.1 + + markdown-extensions@2.0.0: {} + + markdown-table@3.0.4: {} + + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 + + mdast-util-find-and-replace@3.0.1: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.1 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.0.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.1.3: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.1 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.0 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.2.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.0.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + merge2@1.4.1: {} + + micromark-core-commonmark@2.0.2: + dependencies: + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-table@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.1 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.0 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-mdx-expression@3.0.0: + dependencies: + '@types/estree': 1.0.6 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-extension-mdx-jsx@3.0.1: + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.6 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + vfile-message: 4.0.2 + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.1 + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.6 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + micromark-extension-mdx-expression: 3.0.0 + micromark-extension-mdx-jsx: 3.0.1 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-mdx-expression@2.0.2: + dependencies: + '@types/estree': 1.0.6 + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.2 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.1 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.0.2 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-events-to-acorn@2.0.2: + dependencies: + '@types/acorn': 4.0.6 + '@types/estree': 1.0.6 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + vfile-message: 4.0.2 + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.1 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.0.3: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.1: {} + + micromark@4.0.1: + dependencies: + '@types/debug': 4.1.12 + debug: 4.3.7 + decode-named-character-reference: 1.0.2 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.2 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mimic-function@5.0.1: {} + + mrmime@2.0.0: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.7: {} + + neotraverse@0.6.18: {} + + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + + node-releases@2.0.18: {} + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + oniguruma-to-es@0.4.1: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 5.0.2 + regex-recursion: 4.2.1 + + ora@8.1.1: + dependencies: + chalk: 5.3.0 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@6.1.0: + dependencies: + yocto-queue: 1.1.1 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-queue@8.0.1: + dependencies: + eventemitter3: 5.0.1 + p-timeout: 6.1.3 + + p-timeout@6.1.3: {} + + p-try@2.2.0: {} + + parse-entities@4.0.1: + dependencies: + '@types/unist': 2.0.11 + character-entities: 2.0.2 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.0.2 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-latin@7.0.0: + dependencies: + '@types/nlcst': 2.0.3 + '@types/unist': 3.0.3 + nlcst-to-string: 4.0.0 + unist-util-modify-children: 4.0.0 + unist-util-visit-children: 3.0.0 + vfile: 6.0.3 + + parse5@7.2.1: + dependencies: + entities: 4.5.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pify@4.0.1: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + postcss@8.4.49: + dependencies: + nanoid: 3.3.7 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preferred-pm@4.0.0: + dependencies: + find-up-simple: 1.0.0 + find-yarn-workspace-root2: 1.2.16 + which-pm: 3.0.0 + + prettier@2.8.7: + optional: true + + prismjs@1.29.0: {} + + prompts@2.4.2: + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + property-information@6.5.0: {} + + queue-microtask@1.2.3: {} + + readdirp@4.0.2: {} + + reading-time@1.5.0: {} + + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.6 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.0(acorn@8.14.0): + dependencies: + acorn-jsx: 5.3.2(acorn@8.14.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - acorn + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.6 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.6 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + + regex-recursion@4.2.1: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@5.0.2: + dependencies: + regex-utilities: 2.3.0 + + rehype-parse@9.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-from-html: 2.0.3 + unified: 11.0.5 + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.6 + '@types/hast': 3.0.4 + hast-util-to-estree: 3.1.0 + transitivePeerDependencies: + - supports-color + + rehype-stringify@10.0.1: + dependencies: + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.3 + unified: 11.0.5 + + rehype@13.0.2: + dependencies: + '@types/hast': 3.0.4 + rehype-parse: 9.0.1 + rehype-stringify: 10.0.1 + unified: 11.0.5 + + remark-gfm@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.0.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-mdx@3.1.0: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.1 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.0 + unified: 11.0.5 + vfile: 6.0.3 + + remark-smartypants@3.0.2: + dependencies: + retext: 9.0.0 + retext-smartypants: 6.2.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + request-light@0.5.8: {} + + request-light@0.7.0: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + retext-latin@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + parse-latin: 7.0.0 + unified: 11.0.5 + + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.0.0 + + retext-stringify@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unified: 11.0.5 + + retext@9.0.0: + dependencies: + '@types/nlcst': 2.0.3 + retext-latin: 4.0.0 + retext-stringify: 4.0.0 + unified: 11.0.5 + + reusify@1.0.4: {} + + rollup@4.27.3: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.27.3 + '@rollup/rollup-android-arm64': 4.27.3 + '@rollup/rollup-darwin-arm64': 4.27.3 + '@rollup/rollup-darwin-x64': 4.27.3 + '@rollup/rollup-freebsd-arm64': 4.27.3 + '@rollup/rollup-freebsd-x64': 4.27.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.27.3 + '@rollup/rollup-linux-arm-musleabihf': 4.27.3 + '@rollup/rollup-linux-arm64-gnu': 4.27.3 + '@rollup/rollup-linux-arm64-musl': 4.27.3 + '@rollup/rollup-linux-powerpc64le-gnu': 4.27.3 + '@rollup/rollup-linux-riscv64-gnu': 4.27.3 + '@rollup/rollup-linux-s390x-gnu': 4.27.3 + '@rollup/rollup-linux-x64-gnu': 4.27.3 + '@rollup/rollup-linux-x64-musl': 4.27.3 + '@rollup/rollup-win32-arm64-msvc': 4.27.3 + '@rollup/rollup-win32-ia32-msvc': 4.27.3 + '@rollup/rollup-win32-x64-msvc': 4.27.3 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sax@1.4.1: {} + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + + semver@6.3.1: {} + + semver@7.6.3: {} + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.6.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + + shiki@1.23.1: + dependencies: + '@shikijs/core': 1.23.1 + '@shikijs/engine-javascript': 1.23.1 + '@shikijs/engine-oniguruma': 1.23.1 + '@shikijs/types': 1.23.1 + '@shikijs/vscode-textmate': 9.3.0 + '@types/hast': 3.0.4 + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + sisteransi@1.0.5: {} + + sitemap@8.0.0: + dependencies: + '@types/node': 17.0.45 + '@types/sax': 1.2.7 + arg: 5.0.2 + sax: 1.4.1 + + source-map-js@1.2.1: {} + + source-map@0.7.4: {} + + space-separated-tokens@2.0.2: {} + + sprintf-js@1.0.3: {} + + stdin-discarder@0.2.2: {} + + stream-replace-string@2.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.4.0 + get-east-asian-width: 1.3.0 + strip-ansi: 7.1.0 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom-string@1.0.0: {} + + strip-bom@3.0.0: {} + + strnum@1.0.5: {} + + style-to-object@0.4.4: + dependencies: + inline-style-parser: 0.1.1 + + style-to-object@1.0.8: + dependencies: + inline-style-parser: 0.2.4 + + tinyexec@0.3.1: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + tsconfck@3.1.4(typescript@5.6.3): + optionalDependencies: + typescript: 5.6.3 + + tslib@2.8.1: + optional: true + + type-fest@4.27.0: {} + + typesafe-path@0.2.2: {} + + typescript-auto-import-cache@0.3.5: + dependencies: + semver: 7.6.3 + + typescript@5.6.3: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-find-after@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-modify-children@4.0.0: + dependencies: + '@types/unist': 3.0.3 + array-iterate: 2.0.1 + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.0.0 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-children@3.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + update-browserslist-db@1.1.1(browserslist@4.24.2): + dependencies: + browserslist: 4.24.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + vite@5.4.11: + dependencies: + esbuild: 0.21.5 + postcss: 8.4.49 + rollup: 4.27.3 + optionalDependencies: + fsevents: 2.3.3 + + vitefu@1.0.3(vite@5.4.11): + optionalDependencies: + vite: 5.4.11 + + volar-service-css@0.0.62(@volar/language-service@2.4.10): + dependencies: + vscode-css-languageservice: 6.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.10 + + volar-service-emmet@0.0.62(@volar/language-service@2.4.10): + dependencies: + '@emmetio/css-parser': 0.4.0 + '@emmetio/html-matcher': 1.3.0 + '@vscode/emmet-helper': 2.10.0 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.10 + + volar-service-html@0.0.62(@volar/language-service@2.4.10): + dependencies: + vscode-html-languageservice: 5.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.10 + + volar-service-prettier@0.0.62(@volar/language-service@2.4.10): + dependencies: + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.10 + + volar-service-typescript-twoslash-queries@0.0.62(@volar/language-service@2.4.10): + dependencies: + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.10 + + volar-service-typescript@0.0.62(@volar/language-service@2.4.10): + dependencies: + path-browserify: 1.0.1 + semver: 7.6.3 + typescript-auto-import-cache: 0.3.5 + vscode-languageserver-textdocument: 1.0.12 + vscode-nls: 5.2.0 + vscode-uri: 3.0.8 + optionalDependencies: + '@volar/language-service': 2.4.10 + + volar-service-yaml@0.0.62(@volar/language-service@2.4.10): + dependencies: + vscode-uri: 3.0.8 + yaml-language-server: 1.15.0 + optionalDependencies: + '@volar/language-service': 2.4.10 + + vscode-css-languageservice@6.3.1: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + + vscode-html-languageservice@5.3.1: + dependencies: + '@vscode/l10n': 0.0.18 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-uri: 3.0.8 + + vscode-json-languageservice@4.1.8: + dependencies: + jsonc-parser: 3.3.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-nls: 5.2.0 + vscode-uri: 3.0.8 + + vscode-jsonrpc@6.0.0: {} + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.16.0: + dependencies: + vscode-jsonrpc: 6.0.0 + vscode-languageserver-types: 3.16.0 + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.16.0: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@7.0.0: + dependencies: + vscode-languageserver-protocol: 3.16.0 + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-nls@5.2.0: {} + + vscode-uri@3.0.8: {} + + web-namespaces@2.0.1: {} + + which-pm-runs@1.1.0: {} + + which-pm@3.0.0: + dependencies: + load-yaml-file: 0.2.0 + + widest-line@5.0.0: + dependencies: + string-width: 7.2.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@9.0.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + xxhash-wasm@1.1.0: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yaml-language-server@1.15.0: + dependencies: + ajv: 8.17.1 + lodash: 4.17.21 + request-light: 0.5.8 + vscode-json-languageservice: 4.1.8 + vscode-languageserver: 7.0.0 + vscode-languageserver-textdocument: 1.0.12 + vscode-languageserver-types: 3.17.5 + vscode-nls: 5.2.0 + vscode-uri: 3.0.8 + yaml: 2.2.2 + optionalDependencies: + prettier: 2.8.7 + + yaml@2.2.2: {} + + yaml@2.6.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@1.1.1: {} + + zod-to-json-schema@3.23.5(zod@3.23.8): + dependencies: + zod: 3.23.8 + + zod-to-ts@1.2.0(typescript@5.6.3)(zod@3.23.8): + dependencies: + typescript: 5.6.3 + zod: 3.23.8 + + zod@3.23.8: {} + + zwitch@2.0.4: {} diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..f157bd1 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/readingTime.js b/readingTime.js new file mode 100644 index 0000000..ca6ca03 --- /dev/null +++ b/readingTime.js @@ -0,0 +1,10 @@ +import getReadingTime from 'reading-time' +import { toString } from 'mdast-util-to-string' + +export function remarkReadingTime() { + return function (tree, { data }) { + const textOnPage = toString(tree) + const readingTime = getReadingTime(textOnPage) + data.astro.frontmatter.readingTime = readingTime + } +} diff --git a/src/components/AboutImage.astro b/src/components/AboutImage.astro new file mode 100644 index 0000000..c3760da --- /dev/null +++ b/src/components/AboutImage.astro @@ -0,0 +1,30 @@ +--- +import { Image } from 'astro:assets' +import aboutImage from '../content/images/about.webp' +--- + +{'tiny + + diff --git a/src/components/BaseHead.astro b/src/components/BaseHead.astro new file mode 100644 index 0000000..0e0e173 --- /dev/null +++ b/src/components/BaseHead.astro @@ -0,0 +1,46 @@ +--- +import '@fontsource-variable/jost' +import '@fontsource-variable/playfair-display' + +import '../styles/preflight.css' +import '../styles/global.css' + +interface Props { + image?: string +} + +const canonicalURL = new URL(Astro.url.pathname, Astro.site) + +const { image = '/blog-placeholder-1.jpg' } = Astro.props + +const title = 'Astro Blog' +const description = 'Welcome to my website!' +--- + + + + + + + + + + + +{title} + + + + + + + + + + + + + + + + diff --git a/src/components/FormattedDate.astro b/src/components/FormattedDate.astro new file mode 100644 index 0000000..1bcce73 --- /dev/null +++ b/src/components/FormattedDate.astro @@ -0,0 +1,17 @@ +--- +interface Props { + date: Date; +} + +const { date } = Astro.props; +--- + + diff --git a/src/components/Nav.astro b/src/components/Nav.astro new file mode 100644 index 0000000..dde13dc --- /dev/null +++ b/src/components/Nav.astro @@ -0,0 +1,127 @@ +--- +const { pathname } = Astro.url + +const routes = [ + { name: 'About', href: '/about' }, + { name: 'Projects', href: '/projects' }, + { name: 'Blog', href: '/blog' }, + { name: 'Contact', href: '/contact' }, +] +--- + + + + diff --git a/src/components/PostAttributes.astro b/src/components/PostAttributes.astro new file mode 100644 index 0000000..63a1b52 --- /dev/null +++ b/src/components/PostAttributes.astro @@ -0,0 +1,39 @@ +--- +import type { CollectionEntry } from 'astro:content' +import FormattedDate from './FormattedDate.astro' + +export type Props = { + post: CollectionEntry<'blog'> + full?: boolean +} + +const { post, full = false } = Astro.props + +const { remarkPluginFrontmatter } = await post.render() +--- + +
+
+ + { + full && post.data.updatedDate && post.data.date !== post.data.updatedDate && ( + <> +
+ + Last update: + + + ) + } +
+
~ {remarkPluginFrontmatter.readingTime.minutes.toFixed(0)} min
+
+ + diff --git a/src/components/PostList.astro b/src/components/PostList.astro new file mode 100644 index 0000000..6c22ace --- /dev/null +++ b/src/components/PostList.astro @@ -0,0 +1,37 @@ +--- +import type { CollectionEntry } from 'astro:content' +import PostPreview from './PostPreview.astro' + +export type Props = { + posts: CollectionEntry<'blog'>[] +} + +const { posts } = Astro.props +--- + +
+ +
+ + diff --git a/src/components/PostPreview.astro b/src/components/PostPreview.astro new file mode 100644 index 0000000..bbb35e9 --- /dev/null +++ b/src/components/PostPreview.astro @@ -0,0 +1,76 @@ +--- +import type { CollectionEntry } from 'astro:content' +import Tags from './Tags.astro' +import PostAttributes from './PostAttributes.astro' +import { Image } from 'astro:assets' + +export type Props = { + post: CollectionEntry<'blog'> +} + +const { post } = Astro.props +--- + +
+ {post.data.coverImage && {'foo'}} + +

+ {post.data.title} +

+ ({ count: 1, name: tag, href: `/tag/${tag}` }))} /> +
+ + diff --git a/src/components/Signature.astro b/src/components/Signature.astro new file mode 100644 index 0000000..5907685 --- /dev/null +++ b/src/components/Signature.astro @@ -0,0 +1,30 @@ +--- +import type { HTMLAttributes } from 'astro/types' + +type Props = HTMLAttributes<'svg'> +--- + + + + + + + + diff --git a/src/components/SkillBar.astro b/src/components/SkillBar.astro new file mode 100644 index 0000000..493d8cb --- /dev/null +++ b/src/components/SkillBar.astro @@ -0,0 +1,39 @@ +--- +export type Props = { + progress: number + title: string +} + +const { progress, title } = Astro.props +--- + +
+ {title} +
+
+ + diff --git a/src/components/SpacedLetters.astro b/src/components/SpacedLetters.astro new file mode 100644 index 0000000..64842a0 --- /dev/null +++ b/src/components/SpacedLetters.astro @@ -0,0 +1,38 @@ +--- +export type Props = { + letters: string + even?: boolean + readable?: boolean +} + +const { letters, readable = false, even = false } = Astro.props +--- + +
+ {even ? [...letters].map((letter) => {letter}) : letters} +
+ + diff --git a/src/components/Tag.astro b/src/components/Tag.astro new file mode 100644 index 0000000..dd0ed44 --- /dev/null +++ b/src/components/Tag.astro @@ -0,0 +1,33 @@ +--- +export type Props = { + name: string + href: string + count: number +} + +const { name, href, count } = Astro.props +--- + + +
+ {name} + {count} +
+
+ + diff --git a/src/components/Tags.astro b/src/components/Tags.astro new file mode 100644 index 0000000..32305ee --- /dev/null +++ b/src/components/Tags.astro @@ -0,0 +1,36 @@ +--- +import type { ComponentProps } from 'astro/types' +import Tag from './Tag.astro' + +export type Props = { + tags: ComponentProps[] + rows?: number +} + +const { tags, rows = 1 } = Astro.props + +const height = rows * 2 +--- + +
+ {tags.map((tag) => )} +
+ + diff --git a/src/consts.ts b/src/consts.ts new file mode 100644 index 0000000..0df8a61 --- /dev/null +++ b/src/consts.ts @@ -0,0 +1,5 @@ +// Place any global data in this file. +// You can import this data from anywhere in your site by using the `import` keyword. + +export const SITE_TITLE = 'Astro Blog'; +export const SITE_DESCRIPTION = 'Welcome to my website!'; diff --git a/src/content/blog/4-useful-typescript-tricks.md b/src/content/blog/4-useful-typescript-tricks.md new file mode 100644 index 0000000..b872733 --- /dev/null +++ b/src/content/blog/4-useful-typescript-tricks.md @@ -0,0 +1,232 @@ +--- +title: '5 useful Typescript tricks' +date: '2019-10-06' +categories: + - 'coding' +tags: + - 'tips-and-tricks' + - 'typescript' +coverImage: './images/amador-loureiro-BVyNlchWqzs-unsplash-scaled.jpg' +--- + +Typescript is a godsend. It is very easy to get started with and for most developers there is no way back once they get the hang of it. Sometimes it can get pretty advanced and intimidating though. + +This is why I decided to share 5 of my favourite typescript tips and tricks you might have needed in the past. Some are super basic, some are bit more advanced. + +**Update** _07 Okt 2019 @ 07:53_ +Reddit user [jakeboone02](https://www.reddit.com/r/typescript/comments/de17xs/5_useful_typescript_tricks_small_tricks_you_might/f2t9prk?utm_source=share&utm_medium=web2x) found an error in the ternary code. + +**Update** _06 Okt 2019 @ 15:06_ +Reddit user [smeijer87](https://www.reddit.com/r/typescript/comments/de17xs/5_useful_typescript_tricks_small_tricks_you_might/f2qveub?utm_source=share&utm_medium=web2x) found an error in the code for null coalescing. + +**Update** _06 Okt 2019 @ 14:47_ +A fiendly reader pointed out excluding interface type are called discriminated unions. + +1. [react higher-order components](#hoc) +2. [smarter constructor](#constructors) +3. [type checking functions](#type-checking-function) +4. [discriminated unions](#excluding) +5. [optional chaining & null coalescing](#future) + +
+ +![](images/amador-loureiro-BVyNlchWqzs-unsplash-scaled.jpg) + +
+ +Photo byΒ [Amador Loureiro](https://unsplash.com/@amadorloureiroblanco?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/search/photos/type?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +## Higher-order Components + +In React [higher order components (HOC)](https://reactjs.org/docs/higher-order-components.html) are very useful tools. Generally they are used to wrap some layout or functionality to some other component. They are simply functions that return another component: basically the same pattern as decorators. + +In typescript it can be confusing how to write them _maintaining the right props_ after wrapping the original component. Here you are: + +``` +import React from 'react' + +function withLayout

(WrappedComponent: React.ComponentType

) { + return (props: P) => ( +

+
+ +
+
+ ); +} +``` + +Note also that when using the `withLayout` you don't need to specify the generic type explicitly, as typescript will inherit from the function parameter. Super handy! + +## Smarter constructors + +Let's start by the building block this is based on. It's a basic Javascript trick, not a typescript exclusive at first. + +``` +class Pizza { + slices: number + name: string + + constructor(init) { + Object.assign(this, init) + } +} + +const pizza = new Pizza({ + slices: 8, + name: 'Margherita', +}) +``` + +What is happening here? With the super handy `Object.assign` simply assigns the object to the class. This is super handy when classes have many constructor parameters. But this is NOT type safe as your IDE/Editor will tell you. How do we fix this? + +``` +import { NonFunctionKeys } from 'utility-types' + +class Pizza { + slices!: number + name?: string + + constructor(init: Pick>) { + Object.assign(this, init) + } + + eat() { + this.slices = 0 + } +} + +const pizza = new Pizza({ + slices: 8, + name: 'Margherita', +}) +``` + +Let me explain what happens: +This leverages the awesome [utility-types](https://github.com/piotrwitek/utility-types) package. We first take all the keys that are not a function, so we don't overwrite the `eat` method of the class. Then we pick those from the general Pizza type. + +This means that `slices` will be required, while `name` will be optional, as they are defined. + +## Type-checking Functions + +Did you know you can write functions to tell typescript what type something is? This is awesome! + +Suppose we have the following interfaces + +``` +interface Food { + name: string +} + +interface Pasta extends Food { + type: 'Spaghetti' | 'Fusilli' +} + +interface Pizza extends Food { + slices: number +} +``` + +Now we could write a `cook` function that accepts both Pasta and Pizza. Typescript itself cannot differentiate between the too. + +``` +function cook(what: Food) { + if(what === Pizza) ???? +} +``` + +Fortunately there is a nice solution built into typescript. + +``` +function isPizza(x: Food | Pizza): x is Pizza { + return x.hasOwnProperty('slices') +} + +function isPasta(x: Food | Pasta): x is Pasta { + return x.hasOwnProperty('type') +} + + +function cook(plate: Food) { + if (isPizza(plate)) { + // Plate is now of type Pizza + putInTheOven(plate) + } + if (isPasta(plate)) { + // Plate is now of type Pasta + putInThePan(plate) + } +} +``` + +Here we define two functions that return `x is Sometype` and return a boolean value based on the input. It's up to you of course to define it properly, but this can be very useful in various situations. + +## Discriminated unions + +``` +type Sqlite = { + type: 'sqlite', + database: string, +} + +type PostgreSQL = { + type: 'postgresql', + database: string, + host: string, + post?: number +} + +type PossibleConfigs = Sqlite | PostgreSQL + +function initialize(config: PossibleConfigs) {} +``` + +This might look like a simple one, but I often see people putting those sorts of types all into the same interface. By separating the different type of objects you make sure that they are safe. Also the autocomplete will thank you. + +## Optional Chaining & Null Coalescing + +This are future features that will be introduced in Typescript 3.7 that are very useful and will not be lived without after the release in early November 2019. + +Optional chaining is an obvious shorthand. Every time you need to check if a property (especially if nested) exists, you need to do lots of repetitive checking. No more! + +``` +a && a.b && a.b.c // 🀬 + +a?.b?.c // πŸš€ +``` + +Null coalescing is also a very useful shorthand. You all know the `||` shorthand, often used to initialise a variable if no value is given. + +``` +const option = something || 'default' + +// Sugar for +const option = !!something ? something : 'default' +``` + +The problem is with values that are actual values, but result as falsy. + +``` +false || 'default' // => 'default' +0 || 'default' // => 'default' +``` + +This is where the Null Coalescing comes in. + +``` +const option = something ?? 'default' // πŸš€ + +// Sugar for +const option = (x === null || x === undefined) + ? 'default' + : x + +0 ?? 'default' // => 0 +false ?? 'default' // => false +``` + +Basically it only assign the default value if the provided one is `null` or `undefined` so that values like `false` or `0` don't get overwritten. diff --git a/src/content/blog/5-jetbrains-tipsntricks-i-wish-id-known-sooner.md b/src/content/blog/5-jetbrains-tipsntricks-i-wish-id-known-sooner.md new file mode 100644 index 0000000..68a14cb --- /dev/null +++ b/src/content/blog/5-jetbrains-tipsntricks-i-wish-id-known-sooner.md @@ -0,0 +1,76 @@ +--- +title: "5 JetBrains tips'n'tricks I wish I'd known sooner" +date: '2019-07-03' +categories: + - 'coding' +tags: + - 'ide' + - 'jetbrains' +coverImage: './images/cards-scaled.jpg' +--- + +Here are some small features that may not be apparent to the newer devs that leverage the JetBrain IDEs. Most of them I discovered by using the. + +1. Double Shift for navigating your codebase +2. cmd/ctrl + shift + f for text search +3. Remote Interpreters +4. Syncing settings +5. Reformatting + +
+ +![](images/cards-1024x567.jpg) + +
+ +Photo byΒ [Matt Flores](https://unsplash.com/@matdflo?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +## Double Shift + +For many including myself this is the primary way to navigate code and files. Simply press shift two times, type in the file, class or function you are searching for and press enter. This is by far the quickest and most accurate way to navigate code in any Editor or IDE I've tried so far. +Do it once, and you will not go back. + +## CMD + Shift + F + +**Windows & Linux**: ctrl + shift + f + +This is somewhat similar to 2x Shift. The main difference is that double shifts searches mainly for filenames and symbols (function names, class names, etc.) while CMD shift f functions more like a text search. + +What makes this really powerful is that you can [regex search](https://www.jetbrains.com/help/idea/tutorial-finding-and-replacing-text-using-regular-expressions.html#Tutorial_Finding_and_Replacing_Text_Using_Regular_Expressions.xml), [mask files by extension](https://www.jetbrains.com/help/idea/finding-and-replacing-text-in-project.html#exclude_type), [exclude folders](https://www.jetbrains.com/help/webstorm/configuring-project-structure.html#022f3834) (e.g. build folders) and search only in specific directories. + +If you ever _loose something in your code_ or _maybe your are new to the project_ and don't know where certain parts are located, **this is the way to find it.** + +## Remote Interpreters + +This is a huge one for me! JetBrains allows you tu run the code on remote machines. This extends also to all the packages you install, the shell in the terminal is automatically opened on the host. + +Remote hosts can be either a machine in which you ssh into, a local docker container running a different version of the language that you need. + +_You might ask why?_ + +1. Use a docker container with a specific version of node/python/php/etc. instead of installing it locally on your machine. Basically a virtual environment for every language. Amazing! +2. Maybe you want to run the code on a Raspberry Pi, which has a different architecture. So all the packages you install will be installed on the Raspberry and when you hit _command+r_ the code will execute not on your machine but you still get the logs. Incredible! + +To configure **simply go to the** _**run**_ **menu and add a new remote interpreter**. + +## Sync Settings across devices + +This is very simple. You can sync all your settings, including plugins, to either your JetBrain account or your own git settings repository. When you open the IDE some where else, everything is back to how it was. + +Enable by going to: File -> Sync IDE Settings + +**Note:** The synchronisation is per-IDE-basis, so your WebStorm settings are not synced with your PyCharm settings of course. + +## Reformatting + +Yet again a one of the reasons why I can't go back to VSCode. For each language there is a TON of customisation possible when reformatting. You can decide how your spaces should look, commas, imports, semicolons, everything is completely up to you. + +You can tinker around with it in the Settings under: Editor -> Code Style -> + +**Bonus:** If you select on a folder in the project view, you can reformat all files inside it, quick and easy. This is especially useful if one has imported some external sources for example. + +Thats it, I hope you found some of it useful and that you can enjoy the JetBrain cosmos even more πŸ˜‰ diff --git a/src/content/blog/a-guide-to-directus-for-gatsby-or-sapper-as-cms.md b/src/content/blog/a-guide-to-directus-for-gatsby-or-sapper-as-cms.md new file mode 100644 index 0000000..6853dca --- /dev/null +++ b/src/content/blog/a-guide-to-directus-for-gatsby-or-sapper-as-cms.md @@ -0,0 +1,266 @@ +--- +title: 'A guide to Directus for Gatsby or Sapper as CMS' +date: '2020-04-11' +categories: + - 'coding' +tags: + - 'cms' + - 'directus' + - 'gatsby' + - 'sapper' + - 'static-generated' +coverImage: './images/noah-silliman-doBrZnp_wqA-unsplash.jpg' +--- + +For those who don't know what [Directus](https://directus.io/) is: an open source database first CMS that generates an api. Lot of buzzwords there, but it's truly a cool project that deserves much more attention IMO. + +Recently I've used it to deliver some static generated websites that needed some sort of CMS. Think of a blog, or small landing pages. For that kind you can combine it with Gatsby or in this case Sapper to generate static html from the API. + +The article will focus on Sapper, but the parts related to Directus are identical for Gatsby, just the frontend will change. + +#### What will we do today? + +1. [Install Directus](#1) +2. [Create some data and make in publicly available](#2) +3. [Create a super small frontend](#3) +4. [Write a custom hook for Directus that automatically triggers the build whenever content changes in the DB.](#4) + +
+ +![](images/noah-silliman-doBrZnp_wqA-unsplash.jpg) + +
+ +Photo byΒ [Noah Silliman](https://unsplash.com/@noahsilliman?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/s/photos/rabbit?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +## Installing Directus + +This should be straight forward. These instructions are adopted from the [official docker guide](https://docs.directus.io/installation/docker.html). I will use Docker for this. + +``` +# docker-compose.yml + +version: "3.7" + +services: + + mysql: + image: mysql:5.7 + volumes: + - ./data/db:/var/lib/mysql + env_file: .env + + directus: + image: directus/directus:v8-apache + ports: + - "8000:80" + env_file: .env + volumes: + - ./data/config:/var/directus/config + - ./data/uploads:/var/directus/public/uploads +``` + +The we run `docker-compose up -d`. After a few seconds we need to initialise Directus. + +``` +docker-compose run directus install --email some@email.com --password 1337 +``` + +Now you can go to [localhost:8000](http://localhost:8000) and sign in with the credentials you just specified + +## Create some data + +Now I'm going to create some data to test our blog. First go to the [settings](http://localhost:8080/admin/#/_/settings/collections) and create a new collection. Im a going to call it `posts`. +Then we are going to add a `title` text field and a simple markdown editor with a `body` field. +Lastly we add a simple post with random data. + +
+ +![](images/data.gif) + +
+ +Insert collection and data + +
+ +
+ +## Giving permissions + +Now we need to give permission to the `public` role so that we don't need an API Key. For the most sites this is perfectly fine, since the data we only expose the data that gets displayed in the website anyways. + +Goto the [roles settings](http://localhost:8080/admin/#/_/settings/roles) and click on `public`. There select the tables you want/need for the website. + +Gotcha: If you have files (like photos) you also need to enable them for public viewing. Do this by clicking "Show Directus System Collections" and enabling view access to `Files` + +
+ +![](images/permissions.gif) + +
+ +Give permissions to the public user + +
+ +
+ +## Building a minimal frontend with sapper + +I will not explain how [Sapper](https://sapper.svelte.dev/) works as this is not the focus today. If you don't know Sapper: It's very similar to Nuxt or Next.js with the additional option to even export as static html, so the end result is similar to a Gatsby website. Very powerful and easy to use and code. + +``` +# Setup +npx degit "sveltejs/sapper-template#rollup" my-blog +cd my-blog +yarn +yarn run dev +# open http://localhost:3000 +``` + +### Load data from Directus + +Directus has a [JS SDK](https://docs.directus.io/guides/js-sdk.html) and since we have made data public we don't even need a token or authentication. Awesome πŸš€ + +``` +yarn add @directus/sdk-js +``` + +First we are going to initialise the SDK. The default project name is simply `directus` + +``` +// ./src/lib/api.js + +import DirectusSDK from '@directus/sdk-js' + +export const client = new DirectusSDK({ + url: 'http://localhost:8000', + project: 'directus' +}) +``` + +Then lets make a server side json loader so that the exported site will not even contact the server afterwards. Completely static html. + +``` +// ./src/routes/posts.json.js + +import { client } from '../lib/api' + +export async function get (req, res, next) { + try { + const { data } = await client.getItems('posts') + + res.writeHead(200, { + 'Content-Type': 'application/json' + }) + res.end(JSON.stringify(data)) + } catch (e) { + res.writeHead(404, { + 'Content-Type': 'application/json' + }) + res.end(JSON.stringify({ + message: 'Not found' + })) + } +} +``` + +Finally the svelte component. + +``` +// ./src/routes/index.svelte + + + + + +
+ {#each data as post} + + {/each} +
+``` + +## Write a custom hook to trigger a build every time the data changes + +When it comes to static generated sites often the easiest way to do things is to simply generate the site every "x" time. That kinda works however there will be many build that don't contain any change and you need to wait for a cron job to see changes. That sucks. + +Fortunately Directus supports writing custom hooks! πŸŽ‰ +I will illustrate the case for [Drone](https://drone.io/), but the approach can be used for any CI/CD server out there. + +For that we create a new php file and give it a name. In my case: `drone-hook.php` + +``` +# ./hooks/drone-hook.php + + [ + 'item.create' => process, + 'item.update' => process, + 'item.delete' => process, + ] + ]; +``` + +I've also put the token inside of the `.env` file so that I can safely check my code into a repo and not having to worry about having a token lying around in the codebase. + +``` +# .env + +... +DIRECTUS_DATABASE_PASSWORD=directus + +DRONE_TOKEN=my-drone-token +``` + +The last thing to do is actually load the code into Directus. You can simply mount the `./hooks` folder we just created into the container and reload. + +``` +# docker-compose.yml + +version: "3.7" + +... + + directus: + ... + volumes: + ... + - ./hooks:/var/directus/public/extensions/custom/hooks +``` + +This will trigger a curl post request every time items in a collection listed inside of `$collectionsToWatch` get either created, updated, or deleted. + +You will probably need to make some adaptations if you are not using Drone, but at the end it will boil down to making a http request to your build server triggering a new build. diff --git a/src/content/blog/a-practical-introduction-to-react-hooks.md b/src/content/blog/a-practical-introduction-to-react-hooks.md new file mode 100644 index 0000000..3ead4fe --- /dev/null +++ b/src/content/blog/a-practical-introduction-to-react-hooks.md @@ -0,0 +1,302 @@ +--- +title: 'A practical introduction to React Hooks' +date: '2019-05-03' +categories: + - 'coding' +tags: + - 'hooks' + - 'javascript' + - 'react' +coverImage: './images/matt-artz-353210-unsplash-scaled.jpg' +--- + +Since [React](https://reactjs.org/) 16.8 was published in February Hooks are now officially supported and the API finalised and stable. They arose around the idea of functional programming. In short: they allow to have state in functional components and with custom hooks (we'll have a look at those later) they allow us to reuse and share state logic between multiple components. This article assumes a basic understanding of React. + +All the code shown can be found here: [https://git.nicco.io/cupcakearmy/guide-react-hooks](https://git.nicco.io/cupcakearmy/guide-react-hooks) + +
+ +![](images/matt-artz-353210-unsplash-1024x780.jpg) + +
+ +wrenches - new tools + +
+ +
+ +#### What we will look at today + +1. Class Components vs Functional Components +2. Native React hooks + - `useState` + - `useEffect` + - `useRef` +3. Custom hooks + - `useWindow` + - `useApi` (The real power) + +## 1\. Class vs Functional + +Let's first have a look at the 'hello world' of react: A simple counter which we can increment or decrement. + +###### Class + +``` +import React from 'react' + + +class SimpleClass extends React.Component { + + constructor(props) { + super(props) + this.state = { + counter: 0, + } + } + + componentDidMount() { + console.log('Lets goo πŸš€') + setTimeout(() => this.setState({ counter: 5 }), 2000) + } + + componentDidUpdate() { + console.log(this.state.counter) + } + + render() { + return
+
{this.state.counter}
+
+ + +
+ } +} +``` + +Easy! Now we will convert the snippet above to the functional equivalent with the help of hooks. + +###### Hooks + +``` +import React, { useEffect, useState } from 'react' + + +const SimpleFC = () => { + const [counter, setCounter] = useState(0) + + return
+
{counter}
+
+ + +
+} +``` + +Awesome πŸš€ Simple enough right? + +## 2\. Native React hooks + +### useState + +Our constructor with `state` is gone and we have a simple `const [counter, setCounter] = useState(0)`. + +How does this work? `useState` returns an array wich deconstructed gives us a getter and a setter. The parameter we pass to it is the initial value. That is all. Simple and useful. + +### useEffect + +How about the timeout and the `console.log`? Welcome `useEffect`! + +`useEffect` takes a function and executes it every time the component updates. So it is basically `componentDidMount` and `componentDidUpdate` together. +The second parameter determines when the function will be triggered. It expects an array and checks whether the variables inside it change. +If no array is passed it will trigger every time the component gets updated and or mounted. +This means that you can pass props into the array and it will effect only when those change. Also, if you pass an empty array it will trigger only once and is equivalent to `componentDidMount`. + +``` +useEffect(myFN) // triggered every time the component gets updated +useEffect(myFN, []) // Only triggered +useEffect(myFN, [prop1, prop2]) // Gets triggered when either the props get changed +``` + +In our example from above we would use it as follows: + +``` +import React, { useEffect, useState } from 'react' + + +const SimpleFC = () => { + const [counter, setCounter] = useState(0) + + useEffect(() => { + setTimeout(() => { + setCounter(5) + }, 1000) + }, []) + + useEffect(() => { + console.log(counter) + }) + + return
+
{counter}
+
+ + +
+} +``` + +### useRef + +Now let's have a look at `useRef`. We will have a normal class based component and the equivalent functional one with the help of hooks. + +###### Class + +``` +class RefClass extends React.Component { + constructor(props) { + super(props) + this.myRef = React.createRef() + this.change = this.change.bind(this) + } + + change() { + this.myRef.current.style.backgroundColor = '#6ba7ee' + } + + render() { + return
+ +

+
+
+ } +} +``` + +###### Hooks + +``` +const RefFN = () => { + const rect = useRef() + + const change = () => rect.current.style.backgroundColor = '#6ba7ee' + + return
+ +

+
+
+} +``` + +That is huge improvement in terms of amount code and most importantly readability. `react.current` points to the Dom element, which we can then modify at our will. + +As a side node: look how much cleaner we can have class functions. instead of needing binding the function to `this`, in functional components we just need to define them. + +## 3\. Custom hooks + +This is where the real power lies. With custom hooks react allows you to reuse stateful logic and share it between components. Very powerful. +We will cover two examples: + +1. Window size +2. Consume an API + +### Window size + +Assume you want to make a component dependent on the window size of the browser. With react hooks this is quick, easy and reusable. + +###### hooks.js + +``` +export const useWindowSize = () => { + const getCurrentSize = () => ({ height: window.innerHeight, width: window.innerWidth }) + + const [size, setSize] = useState(getCurrentSize()) + + useEffect(() => { + const handle = () => setSize(getCurrentSize()) + window.addEventListener('resize', handle) + return () => window.removeEventListener('resize', handle) + }) + + return size +} +``` + +###### component.jsx + +``` +import { useWindowSize } from '../Hooks' + +const Custom = ()=> { + const size = useWindowSize() + + return
+ Width: {size.width} +
+ Height: {size.height} +
+} +``` + +As we can see we created a custom hook called `useWindowSize`. We now can use our own hook inside of other components. + +Custom components are just arrow functions that use the native `useState` and `useEffect` and some custom logic you add. + +Note the `return () => window.removeEventListener('resize', handle)` inside the effect function. You can return a function in the effect function that will get called whenever the hook will be unmounted. This allows us to do cleanup. In this case we stop listening for window size changes. Neat πŸ’ͺ + +### API Hook + +Last but definitely not least: API calls. I personally think this is where hooks really show their power. I'll show you the code first and then explain. + +###### hooks.js + +``` +export const useCallApi = (url) => { + const [data, setData] = useState() + + const update = () => { + fetch(url) + .then(response => response.json()) + .then(json => setData(json)) + } + + useEffect(() => { + update() + }, []) + + return [data, update] +} +``` + +###### posts.jsx + +``` +import { useCallApi } from '../Hooks' + + +const Posts = () => { + const [posts] = useCallApi(`https://jsonplaceholder.typicode.com/posts`) + const [users] = useCallApi(`https://jsonplaceholder.typicode.com/users`) + // ... + + if (!posts) return
Loading πŸ•°
+ + return
+ {posts.map((post, i) =>
+

{post.title}

+

{post.body}

+
)} +
+} +``` + +What is happening? We created a custom hook that queries an API and returns the result. How? We pass a url to the hook and we get the data back. + +Internally the hook uses `useState` to save the results. It executes the update functions once (because the use effect has an empty array as second parameter). + +Now we can use the `useCallApi` hook in multiple components or many times inside the same component. Options are endless. diff --git a/src/content/blog/a-sane-and-efficient-guide-for-consuming-graphql-endpoints-in-typescript.md b/src/content/blog/a-sane-and-efficient-guide-for-consuming-graphql-endpoints-in-typescript.md new file mode 100644 index 0000000..ddfb3a0 --- /dev/null +++ b/src/content/blog/a-sane-and-efficient-guide-for-consuming-graphql-endpoints-in-typescript.md @@ -0,0 +1,199 @@ +--- +title: 'A sane and efficient guide for consuming GraphQL endpoints in Typescript' +date: '2021-12-31' +categories: + - 'coding' +tags: + - 'code-generation' + - 'graphql' + - 'typescript' +coverImage: './images/clayton-robbins-Ru09fQONJWo-unsplash-scaled.jpg' +--- + +GraphQL is becoming common practice in the wild, while I feel the workflow with Typescript is still not straight forward. I want to propose one way to go about it and hopefully make your next Typescript GraphQL project a joy to work with! +Lets dive deeper 🀿. + +I created a tiny [companion repository](https://github.com/cupcakearmy/blog-typescript-graphql) if you want to check out the code and try it out. +Or check out the [finished demo](https://blog-typescript-graphql.vercel.app/). + +
+ +![](images/clayton-robbins-Ru09fQONJWo-unsplash-1024x683.jpg) + +
+ +Photo by [Clayton Robbins](https://unsplash.com/@claytonrobbins?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/@claytonrobbins?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +## Intro + +First we need to decide on what do we want (and probably need) + +- Editor support for syntax highlighting `gql` and `.graphql` files. +- Strict type safety for our client. +- Easy tooling & workflow + +So our workflow will look something like this: + +``` +GrapQL API -> Schema -> Queries & Mutations -> Typescript -> Client +``` + +For this article we'll build a minuscule one pager using the [SpaceX Land GraphQL API](https://api.spacex.land/graphql/) to display some space travel data. + +## Editor setup + +The setup will be be for VSCode. For that we first install the [GraphQL extension](https://marketplace.visualstudio.com/items?itemName=GraphQL.vscode-graphql). This will enable us to have warnings and autocompletion inside of `gql` tags and `.graphql` files. + +We need to add a `.graphqlrc.yml` file at the root with the following content: + +``` +schema: https://api.spacex.land/graphql/ +``` + +## Writing Queries & Mutations + +Now onto the real stuff. +We want to take our endpoint, generate types and queries from it that can then be used by Typescript safely. To do that we will: + +1. Setup generators for Schema, Queries, Mutations & SDK. +2. Write some Queries & Mutations +3. Generate the SDK +4. Consume the SDK + +### Setup + +There is this amazing project called `@graphql-codegen` which is a collection of tools for helping you generating various things from GraphQL. Let's install: + +``` +# Generators +pnpm i -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-graphql-request +# For the SDK +pnpm i -D graphql graphql-request graphql-tag +``` + +I will assume my GraphQL stuff will live under `./src/lib/gql` + +We will create a top level configuration file to handle all of our generation step called `codegen.yaml`. Ignore the `config` option for now, I will explain that later + +``` +schema: https://api.spacex.land/graphql/ +documents: "src/**/*.graphql" +generates: + ./src/lib/gql/gen.ts: + plugins: + - "@graphql-codegen/typescript" + - "@graphql-codegen/typescript-operations" + - "@graphql-codegen/typescript-graphql-request" + config: + maybeValue: "T" + typesPrefix: GQL + immutableTypes: true + useTypeImports: true + avoidOptionals: true +``` + +The property `schema` does not need an explanation. +`generates` has 3 plugin enabled, one for the general types, another for queries and mutations and the last one to generate us a ready to use SDK and will save it under `./src/lib/gql/gen.ts`. +`documents` is a glob that will find all our GraphQL files we write and generate the according code. + +### Creating Queries + +Now let's create a `src/lib/gql/root.graphql` file and write some queries, all autocompleted of course! + +``` +query LaunchpadsMany { + launchpads(limit: 10) { + id + name + location { + name + } + successful_launches + status + } +} + +query LaunchByYear($year: String!) { + launches(find: { launch_year: $year }) { + mission_id + mission_name + launch_date_utc + rocket { + rocket_name + } + } +} +``` + +### Let magic do it's thing + +``` +pnpm exec graphql-codegen +``` + +This will look at all our custom queries and mutations and generate us a ready to consume SDK that is completely typed. Amazing! + +### Leverage the new SDK + +``` +// src/lib/gql/index.ts + +import { GraphQLClient } from 'graphql-request' +import { getSdk } from './gen' + +const client = new GraphQLClient('https://api.spacex.land/graphql/') +export const SDK = getSdk(client) +``` + +``` +import { SDK } from '$lib/gql' + +const data = await SDK.LaunchByYear({ year: '2021' }) +``` + +You can also use the generated types to explicitly set them + +``` +import { SDK } from '$lib/gql' +import type { GQLLaunchByYearQuery } from '$lib/gql/gen' + +const data: GQLLaunchByYearQuery = await SDK.LaunchByYear({ year: '2021' }) +``` + +Every thing is typed now, I can't pass a number to the `year` variable or use return data that does not exist. Typescript will error on me. This not only gives us autocompletion but also the safety of what we are doing. + +### Configuration options + +I promised I would come back to it at some point. + +``` +schema: ... +generates: + ... + config: + maybeValue: "T" + typesPrefix: GQL + immutableTypes: true + useTypeImports: true + avoidOptionals: true +``` + +There are [many options](https://www.graphql-code-generator.com/plugins/typescript#config-api-reference) for the generators, but I think these are quite sensible defaults. + +`maybeValue` is `T | null` as default, but since we only use our queries which are type safe we can just remove uncertainty and use the correct type straight away. + +`avoidOptionals` same thing as the `maybeValue`, just with `prop:?`. Don't want that. + +`typesPrefix` is useful if you have some own type specifications that you don't want to clash with. I like to prefix all my generated GraphQL stuff with `GQL` to keep it tidy. + +`immutableTypes` i prefer using an immutable type, with basically adds a `readonly` to every property. This way we are sure we are not editing data on the client. + +`useTypeImports` this uses `import type` whenever possible. + +## Final thoughts + +I hope this made your GraphQL life a bit easier, it definitely did for me and it's way more fun to consume GraphQL API this way. Also something worth mentioning is that you can use the `@graphql-codegen/typescript-generic-sdk` package instead of the `@graphql-codegen/typescript-graphql-request` if you want to do the network requests yourself. It's easy to use but if you don't really have a reason just stick with the `graphql-request` one I'd say. diff --git a/src/content/blog/automate-github-releases-with-drone.md b/src/content/blog/automate-github-releases-with-drone.md new file mode 100644 index 0000000..d41fba4 --- /dev/null +++ b/src/content/blog/automate-github-releases-with-drone.md @@ -0,0 +1,137 @@ +--- +title: 'Automate Github releases with Drone.' +date: '2020-01-29' +categories: + - 'coding' +tags: + - 'cd' + - 'drone' +coverImage: './images/franck-v-U3sOwViXhkY-unsplash-scaled-1.jpg' +--- + +If you have a project on github that has releases for code or binaries for example it might be a good idea to automate it. Not only this saved a lot of clicks and time, but also it makes releases predictable and therefore less prone to errors in the process. + +For this article I will take my own [project](https://github.com/cupcakearmy/autorestic) as the example here, but of course this can be applied to any project, written in whatever language and/or framework. + +Also I will base this guide on [Drone](https://drone.io/). But I'm sure there is the same workflow for jenkins/circle/whatever CI/CD system you are using. +This means I'm assuming you have a repository already running with Drone. + +
+ +![](images/franck-v-U3sOwViXhkY-unsplash-scaled-1.jpg) + +
+ +Photo byΒ [Franck V.](https://unsplash.com/@franckinjapan?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/s/photos/robot?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +The first thing we will need is an access token for the Github API. +You can get them here [https://github.com/settings/tokens](https://github.com/settings/tokens). I called my `Drone` and you need to check the permissions for the repos as follows. + +
+ +![](images/Screenshot-2020-01-29-at-14.57.05.png) + +
+ +How to create a new token in Github + +
+ +
+ +Copy the token and save it somewhere **safe**. You will see it only once. + +We will add this token to our Drone repository settings. For that navigate to your drone instance and open the settings for the repository in question. + +
+ +![](images/Screenshot-2020-01-29-at-14.55.28.png) + +
+ +Add the token to Drone secrets + +
+ +
+ +I've called my secret `github` and I have not allowed it in PRs. Otherwise a PR made by some random user could trigger a release. We don't want that. + +Now it's time to edit our drone file and make everything automatic. The flow at the end will be as follows. + +1. Code, commit and develop +2. When you are ready for the next release we create a tag +3. Once a tag is created and pushed drone will automatically build and release that code attached to the tag. + +Simple right? Lets see how! + +``` +# .drone.yml +--- +kind: pipeline +name: default + +steps: +- name: build + image: node + pull: always + commands: + - yarn + - yarn run bin + when: + event: tag + +- name: publish + image: plugins/github-release + pull: always + settings: + api_key: + from_secret: github + files: bin/* + checksum: + - sha512 + note: CHANGELOG.md + when: + event: tag +--- +kind: signature +hmac: 3b1f235f6a6f0ee1aa3f572d0833c4f0eec931dbe0378f31b9efa336a7462912 + +... +``` + +Lets understand what is happening here: + +First I'm building my project. In this case this is a standalone typescript executable build by [pkg](https://github.com/zeit/pkg). The build binaries will be emitted into the `./bin` folder. But it really does not matter. Could be anything. + +Secondly we tell the [Github release plugin](http://plugins.drone.io/drone-plugins/drone-github-release/) what files we want to include in the release. In my case this was everything inside the `bin` folder. This can also be an array. + +``` +files: + - dist/* + - bin/binary.exe +``` + +The `api_key` includes the token, which we load from a secret so that we don't simply put in the `.drone.yml` file, which could be a huge security issue! + +The `checksum` setting is also amazing because as the name suggests the plugin automatically generates checksums for all the files. That is amazingly practical and there is no reason not to do that. You can choose a few hash functions but I would suggest simply going with `sha512`. + +## So how do a trigger a release now? + +Simple! First tag your code with the following command + +``` +git tag 1.2.3 +``` + +Now push the tag and drone will be on its way + +``` +git push --tags +``` + +Thats it! Hope it made your release journey easier πŸ™‚ diff --git a/src/content/blog/backup-mongodb-inside-of-docker-the-right-way.md b/src/content/blog/backup-mongodb-inside-of-docker-the-right-way.md new file mode 100644 index 0000000..bfc589b --- /dev/null +++ b/src/content/blog/backup-mongodb-inside-of-docker-the-right-way.md @@ -0,0 +1,67 @@ +--- +title: 'Backup MongoDB inside of Docker the easy way' +date: '2019-08-15' +categories: + - 'coding' +tags: + - 'cli' + - 'docker' +coverImage: './images/tobias-fischer-PkbZahEG2Ng-unsplash-scaled.jpg' +--- + +Backing up a mongo instance is more confusing than it should be. Maybe you have run into a `the input device is not a TTY` or you simply don't know how to do it? Here are two 1-Liner to backup and restore a running mongo instance. + +
+ +![](images/tobias-fischer-PkbZahEG2Ng-unsplash-1024x497.jpg) + +
+ +Photo byΒ [Tobias Fischer](https://unsplash.com/@tofi?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/search/photos/database?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +## Setup + +First we define our mongo instance like below. Notice that instead of mapping the data directory onto our filesystem we have a native volume. + +###### docker-compose.yml + +``` +version: '3.7' + +volumes: + db: + +services: + db: + image: mongo:3-xenial + restart: always + volumes: + - db:/data/db + ports: + - 27017:27017 +``` + +Then start with `docker-compose up -d`. + +## Backup + +First we will do a backup of our running instance. + +``` +docker-compose exec -T db mongodump --archive --gzip --db mydb > dump.gz +``` + +The `-T` option is for enabling piping the output to our own machine. We also tell mongo to use the `--gzip` option to compress the file significantly. +Lastly we specify the `--db ` that we want to backup. + +## Restore + +Whenever we want to restore a db, or maybe seed it we can run the following: + +``` +docker-compose exec -T db mongorestore --archive --gzip < dump.gz +``` diff --git a/src/content/blog/be-your-own-tiny-image-cdn.md b/src/content/blog/be-your-own-tiny-image-cdn.md new file mode 100644 index 0000000..b975986 --- /dev/null +++ b/src/content/blog/be-your-own-tiny-image-cdn.md @@ -0,0 +1,98 @@ +--- +title: "Be your own (tiny) image CDN" +date: "2023-04-28" +--- + +Today, I want to share how to create and host your own image transformation service, much like the known [Imgix](https://imgix.com/) and [Cloudinary](https://cloudinary.com/). The aim is to have a powerful transformation server for images that caches, so images only need to be computed once. + +The building blocks will be [imgproxy](https://github.com/imgproxy/imgproxy) and [nginx](https://nginx.org/). The former is a battle tested and fast image server with support for most image operations, while nginx should not need an introduction. + +
+ +![](images/meagan-carsience-QGnm_F_nd1E-unsplash1-1024x683.jpg) + +
+ +Photo by [Meagan Carsience](https://unsplash.com/@mcarsience_photography?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/photos/QGnm_F_nd1E?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +While imgproxy is the core of this operation, it does not support caching. This is intentional, as it's intended to be run behind a proxy. For that, nginx is the tool of choice, as it enables us to easily setup caching rules to avoid generating the same image twice in a given cache interval. Everything will be done in docker containers, but the concept, of course, extends to bare metal too. + +## Setup + +Imgproxy fortunately is very customisable and options can be passed by env variables, which is wonderful. + +It's generally advised to use signed URLs if possible. In my case, there was no backbend that could sign them, so it was avoided. Whenever omitting signing, it is critical to limit the allowed sources to the minimum with `IMGPROXY_ALLOWED_SOURCES` so that it cannot be abused by other websites. + +Below is docker file used. Required is only the `IMGPROXY_BIND` as otherwise nginx cannot connect to our image container. The other options are up to you and are just here for a quick setup. + +``` +# docker-compose.yaml +version: '3.8' + +volumes: + cache: + +services: + img: + image: darthsim/imgproxy + environment: + # Required for nginx + IMGPROXY_BIND: 0.0.0.0:80 + + # Security + IMGPROXY_MAX_SRC_RESOLUTION: 100 + IMGPROXY_ALLOWED_SOURCES: https://images.example.org/ + + # Transforms + IMGPROXY_ENFORCE_WEBP: true + IMGPROXY_ENFORCE_AVIF: true + IMGPROXY_ONLY_PRESETS: true + IMGPROXY_PRESETS: default=resizing_type:fit,sm=size:250:250,md=size:500:500,lg=size:1000:1000 + + proxy: + image: nginx + ports: + - 80:80 + volumes: + - ./proxy.conf:/etc/nginx/conf.d/default.conf:ro + - cache:/tmp +``` + +The more interesting part is the nginx configuration file below. In this case, we target 30 days as a cache TTL. This could be easily increased if we are only talking about static images. + +``` +# Set cache to 30 days, 1GB. +# Only use the uri as the cache key, as it's the only input for imageproxy. +proxy_cache_path /tmp levels=1:2 keys_zone=images:8m max_size=1g inactive=30d; +proxy_cache_key "$uri"; +proxy_cache_valid 200 30d; + +server +{ + listen 80; + server_name _; + + location / + { + proxy_pass_request_headers off; + proxy_set_header HOST $host; + proxy_set_header Accept $http_accept; + + proxy_pass http://img; + + proxy_cache images; + } +} +``` + +Here we are configuring a few things, so let's elaborate: + +First a cache is configured at the location `/tmp`, with the name `images`, a maximum size of 1 gigabyte and the `inactive` parameter to 30 days. +For the cache key, we use only the `$uri` variable, as all the parameters that affect image generation are included in the path and makes therefore the image transformation unique. +Lastly, we tell nginx to cache all responses with code `200` for 30 days. + +Another important trick is to strip all headers that reach the proxy. This is done by setting `proxy_pass_request_headers` and only passing the `Accept` header, as it's required for automatically determining the image format. diff --git a/src/content/blog/cleanup-downloaded-google-photos-takeout-archives.md b/src/content/blog/cleanup-downloaded-google-photos-takeout-archives.md new file mode 100644 index 0000000..67547a2 --- /dev/null +++ b/src/content/blog/cleanup-downloaded-google-photos-takeout-archives.md @@ -0,0 +1,79 @@ +--- +title: 'Cleanup downloaded Google Photos Takeout archives' +date: '2019-05-04' +categories: + - 'general' +tags: + - 'google-photos' + - 'google-takeout' + - 'icloud-photos' + - 'migration' +coverImage: './images/rayan-almuslem-1302778-unsplash-scaled.jpg' +--- + +Recently I've been taking my tin foil hat a bit more seriously and since I mostly live in the Apple ecosystem (yes, you can judge me) the iCloud Photos felt like a pretty good alternative. Yes, it's still a cloud but the content [is encrypted](https://support.apple.com/en-us/HT202303) and most importantly Apple has no real economical incentive on data mining you data. They are far ahead in terms of privacy. With that out of the way let's go! πŸš€ + +TLDR: I wrote this [cleaning script](https://gist.github.com/CupCakeArmy/51070b311e6fd0a3f2d793bee3350ede) (tested only on macOS) to remove all duplicates from the [Google Takeout](https://takeout.google.com/) folders. + +
+ +![](images/rayan-almuslem-1302778-unsplash-1024x683.jpg) + +
+ +Photo byΒ [Rayan Almuslem](https://unsplash.com/photos/_aPYcEKtDQ0?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/search/photos/camera?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +The process seemed easy at first. Google offers an awesome tools for exporting data out of their servers. It's called [Takeout](https://takeout.google.com/). So basically you select the Google Photos service and let them create the archives. Then after a few hours you can download them. + +Now at the time I had ~40gb worth of pictures and videos saved in Googles cloud, however the archives I downloaded where about ~90gb. I started looking into it and a lot of photos where duplicates and edited versions that google was keeping. In addition the folders where full of JSON metadata. + +**time for cleanup 🧹** + +Fortunately for us there is the awesome `find` command that will save our lives. Removing metadata and duplicates from more than ~50k files is impossible by hand. + +First we need to remove all `.json` files: + +``` +find ./my_takeout_folder -name "*.json" -type f -delete +``` + +Then all the duplicates that contain a `(1)` at the end of the file. + +``` +# macOS +find -E ./my_takeout_folder -regex ".*\([0-9]+\).*" -type f -delete + +# Unix (Thanks to Stravos F. for pointing that out ❀️) +find ./my_takeout_folder -regextype posix-extended -regex ".*\([0-9]+\).*" -type f -delete +``` + +All the edited photos by google + +``` +find ./my_takeout_folder -name "*edited*" -type f -delete +``` + +And lastly remove all the empty folders. + +``` +find ./my_takeout_folder -type d -empty -delete +``` + +You probably will have multiple folders because you will have to download multiple archives. Simply unpack them all into one folder and run the scripts on to that folder. + +If you are to lazy to run them manually just get this script I wrote + + + +Then... + +``` +chmod +x ./clean +./clean my_folder_with_all_the_google_takouts +``` + +Finally just drag and drop into the Photos app. diff --git a/src/content/blog/create-a-qr-code-for-google-drive.md b/src/content/blog/create-a-qr-code-for-google-drive.md new file mode 100644 index 0000000..56f5f76 --- /dev/null +++ b/src/content/blog/create-a-qr-code-for-google-drive.md @@ -0,0 +1,82 @@ +--- +title: "Create a QR code for Google Drive" +date: "2022-03-17" +--- + +So you want to make a QR code to a google drive file? It's actually quite easy, I'll show you! + +## 1\. Upload the file and get the shared link + +As shown in the video below the first thing is to upload your file (in this case a PDF) and create a sharable link. + +
+ +
+ +Uploading and generating a link for a google drive file + +
+ + + +
+ +## 2\. Convert the link to a download link + +``` +https://drive.google.com/file/d/1LZ09_aJnGy1aHY0DEuOEFGU4mon2ijir/view?usp=sharing +``` + +If we simply use the provided link (example above) it won't download the file, but create a preview of it. + +If we want a direct download we need to change it to that below: + +``` +https://drive.google.com/uc?export=download&id=1LZ09_aJnGy1aHY0DEuOEFGU4mon2ijir +``` + +To summarise: + +``` +https://drive.google.com/file/d//view?usp=sharing +⬇️ +https://drive.google.com/uc?export=download&id= +``` + +Note that the _``_ part will be different for your file. The rest is the same. + +## 3\. Create the QR Code + +To create a QR code there is a very good free website called: [the-qrcode-generator.com](https://www.the-qrcode-generator.com/). Here you simply paste the link and get your QR Code. + +
+ +![](https://api.nicco.io/wp-content/uploads/2022/03/QR-Big.svg) + +
+ +Big QR code + +
+ +
+ +## 4\. Make the QR code smaller and track clicks + +If you want to have a smaller and cleaner QR code you can use a URL shortener like [Cuttly](https://cutt.ly/) to do so. With Cuttly the URL gets shorter and you can see how many people clicked on it. The new link and QR code then look something like this: + +``` +https://cutt.ly/CSonJs9 +``` + +
+ +![](https://api.nicco.io/wp-content/uploads/2022/03/QR-Small.svg) + +
+ +Small QR code + +
+ +
diff --git a/src/content/blog/going-beyond-npm-meet-yarn-pnpm.md b/src/content/blog/going-beyond-npm-meet-yarn-pnpm.md new file mode 100644 index 0000000..1b2604d --- /dev/null +++ b/src/content/blog/going-beyond-npm-meet-yarn-pnpm.md @@ -0,0 +1,96 @@ +--- +title: 'Going beyond NPM: meet Yarn & pnpm' +date: '2019-08-27' +categories: + - 'coding' +tags: + - 'javascript' + - 'node' + - 'npm' + - 'pnpm' + - 'yarn' +coverImage: './images/ruchindra-gunasekara-GK8x_XCcDZg-unsplash-scaled.jpg' +--- + +If you are a JS developer you probably use NPM multiple times a day without thinking about it. It's the default package manager which ships with node. + +But have you wondered what if there was another way of managing your (probably too many πŸ˜‰) packages? We will look at [yarn](https://yarnpkg.com/en/) and [pnpm](https://pnpm.js.org/) as worthy rivals. + +**Update** _27 Aug 2019 @ 21:23_ +As [this user](https://www.reddit.com/r/javascript/comments/cw64xt/going_beyond_npm_meet_yarn_pnpm/ey92a0i?utm_source=share&utm_medium=web2x) on reddit pointed out npm now supports offline installs too, so that part is the same for all three package managers. Also apparently the checksums, but I could now verify it. + +**Update** _27 Aug 2019 @ 22:51_ +If you are having troubles with pnpm try using `pnpm i shamefully-flatten`. Thanks to [this reddit user](https://www.reddit.com/r/node/comments/cw64qq/going_beyond_npm_meet_yarn_pnpm/ey9aa1v?utm_source=share&utm_medium=web2x). + +For the lazy readers: [Jump to the conclusion here](#conclusion). + +
+ +![](images/ruchindra-gunasekara-GK8x_XCcDZg-unsplash.jpg) + +
+ +Photo byΒ [Ruchindra Gunasekara](https://unsplash.com/@ruchindra?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +You might wonder now: why? _Why should I bother reading this when NPM works perfectly_? Is this just another run to the latest framework? Don't worry: there are actual reasons you might want to switch. + +#### Speed!!... or the lack of it? + +The biggest issue that plagues npm is speed. Unfortunately even with the latest version (6) npm is slow. If you ever had to delete the node_modules folder to do a clean install on a bigger project you will know what I mean. Fans start to spin, laptop gets warm and you can go read an article while npm chews on the dependencies. + +## Yarn to the rescue + +Yarn came along in the early days and you definitely have heard about it wandering across Github. Back in the days (before npm 5 with the `package-lock.json`) Yarn addressed the issues with consistency by being the first to generate a lockfile (`yarn.lock`). This file could be checked in and the devs would have a consistent dependencies across multiple machines. + +#### Speed + +Yarn is often twice as fast as npm. It's truly impressive and you need to see it for yourself to believe it. The CLI output is also way more human-friendly. + +#### Offline + +Every package version is only downloaded once, so if you happen to loose connection or need to download the same package again you will gain a substantial speed boost since they are cached locally. +_See update at the top_. + +#### yarn upgrade-interactive + +This is incredible 😍. If you run `yarn upgrade-interactive` you get an interactive CLI where you can choose what packages to upgrade and which not. It's a simple thing, but one you cannot live without anymore if tried it once. + +#### yarn why + +Similar to the previous command this is a very handy cli goodie. simply run `yarn why some-package` and yarn will tell you why it was installed, from which dependency it came from, etc. + +#### Lack of npx + +Unfortunately Yarn lacks the `npx` equivalent of npm, which is the only drawback I encountered while using yarn. Other than that yarn is a very fast and a solid alternative to npm. + +## PNPM: The underdog + +I truly love this project so I might be biased. They basically implemented a thought I had a while back: **reuse the same packages across your computer**. Confused? Let me explain: + +Have you ever measured the size of the your node_modules? + +``` +du -sh node_modules +# --> 816M node_modules +``` + +What?! 0.8Gb for a react-native project?! +Unfortunately that is a pretty common reality and **pnpm** aims to solve that. + +PNPM links your packages with symlinks. This means that **the same version of a package only exists once** on your computer. If you ever install the same package twice, it will simply symlinked to your node_modules. πŸš€ +[On top of that it's even faster than yarn.](https://github.com/pnpm/benchmarks-of-javascript-package-managers) + +#### So perfection is achieved? Let's all switch to pnpm? + +Unfortunately it's not that easy. If you start a new project you can probably go with pnpm, but with existing projects I had some problems with building my apps. So it's definitely experimental at best and should not be used without rigorous testing as it might break your app. pnpm also supports npx with `pnpx`. + +## Conclusion Time + +
SpeedNPXOfflineWell supported
npmπŸŒβœ…βœ…βœ…
yarnπŸš„βŒβœ…βœ…
pnpmπŸš€βœ…βœ…βŒ
+ +As you can see above there is no clear winner. NPM is the most compatible of course but really falls behind in terms of speed. **Yarn in my opinion is currently your best bet** and fallback to `npx your-command` when npx is needed. +pnpm is an incredibly cool tool but is not ready yet for production. With react-native I can cause problems, but with the "normal" stacks it works very good. I will use pnpm for my personal projects from now on. diff --git a/src/content/blog/how-to-avoid-killing-your-macbook-laptop-battery.md b/src/content/blog/how-to-avoid-killing-your-macbook-laptop-battery.md new file mode 100644 index 0000000..da32df0 --- /dev/null +++ b/src/content/blog/how-to-avoid-killing-your-macbook-laptop-battery.md @@ -0,0 +1,171 @@ +--- +title: 'How to avoid killing your MacBook / Laptop battery' +date: '2019-07-23' +categories: + - 'general' +tags: + - 'battery' + - 'laptop' + - 'macbook' +coverImage: './images/israel-palacio-ImcUkZ72oUs-unsplash-scaled.jpg' +--- + +As of May of 2020 this is no more relevant! macOS 10.15.5 finally addressed this issue by not charging the battery to 100% depending on battery temperature, health and so on πŸš€πŸš€πŸš€ + +There are a lot if misleading wisdom out there about batteries (e.g. it is ok to leave your laptop plugged in). The reasons behind it are pretty interesting and not at all trivial. If you want to know why: keep reading. + +**TLDR;** [Jump to the solution](#solution) + +
+ +![](images/israel-palacio-ImcUkZ72oUs-unsplash-scaled.jpg) + +
+ +Photo byΒ [israel palacio](https://unsplash.com/@othentikisra?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/search/photos/electricity?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +> The worst situation is keeping a fully charged battery at elevated temperatures. +> +> batteryuniversity.com + +Batteries are consumable items. This means the degrade over time and loose their ability to store energy. We will see how and why this happens and how to combat it. + +## Why do batteries degrade? + +In theoretical chemistry your battery should be able to last forever. But in the real world of course that's not possible. There are 2 main killers for batteries: + +1. Heat +2. Cycles (Especially above ~80%) + +### Killer #1: Heat + +With heat the internal materials of the battery start to loose their chemical form and therefore their capacity. Below is a table that illustrates how batteries react to temperature exposure. + +_Estimated recoverable capacity when storing Li-ion for one year atΒ various temperatures._ + +
Temperature40% charge100% charge
0Β°C98% (after 1 year)94% (after 1 year)
25Β°C96% (after 1 year)80% (after 1 year)
40Β°C85% (after 1 year)65% (after 1 year)
60Β°C75% (after 1 year)60%
(after 3 months)
+ +As we can see, a heated battery basically is a lost cause, especially when fully charged. + +Unfortunately **there is little we can do for the heat issue** since the thermal design of our Laptop is fixed and the manufacturer will choose where to place the battery and if and how to cool it. + +### Killer #2: Cycles + +Over this one we have much more control. A cycle varies in definition, but basically it is a discharge, followed by a charge. + +Why are cycles bad? Well whenever we move electrons around our battery either by using it or while charging, the chemical material is subjected to wear. Why exactly this happens is beyond my understanding of chemistry, so I won't try to explain it since I would probably do it wrong. + +What I can tell you is how to charge and use your battery in the correct manner: + +1. Ideally leave your battery between 30% and 80% +2. Don't charge over 80% if not strictly required for a long trip or so. + +### Don't believe me, trust the data + +
+ +![](images/DST-cycles-web2.jpg) + +
+ +Capacity loss as a function of charge and discharge bandwidth. [Source](https://batteryuniversity.com/index.php/learn/article/how_to_prolong_lithium_based_batteries) + +
+ +
+ +As we can observe above, all the tested bandwidth that regularly charged to a full 100% degraded the fastest. Don't do that. + +Part of the problem is that as you can see in the graphic below is that while **the first 80% of the capacity is charged easily and quick, the last 80% to 100% have an exponential curve**. Those last percentages are really taxing on the battery because you are trying to stuff the last electrons inside an almost full battery. Imagine you stuffing a last bag into an almost full garbage. You will need to push it. + +Making matters worse, the battery will heat up during the last steps of charging because of the strain that is undergoing. This only adds to the problem, since as we learned above heat is incredibly bad for capacities. + +
+ +![](images/Battery-Charge-Voltage-vs-Time.png) + +
+ +Charging graph for Lithium Batteries. +Graph by [batteryuniversity.com](https://batteryuniversity.com/index.php/learn/article/charging_lithium_ion_batteries) - Colorised by [Android Authority](https://www.androidauthority.com/maximize-battery-life-882395/) + +
+ +
+ +This is also the reason because electric vehicles only charge up to 80%. It increases the lifespan of the battery significantly. On the other hand, consumer **products like laptops and phones are more about selling you maximum battery life.** What they don't tell you is **how quick that advertised battery life is going to last after 6 months** of usage. + +### Myth: It's ok to keep you laptop plugged in + +This is a misconception that arose in recent years. While it is not completely wrong, it overlooks some important aspects. + +What is true? Modern laptops and phones don't overcharge the battery and will switch to using only the powered cable as the source. **However** at some point the battery will dip below 97% and the laptop will start charge it again to 100%. Assuming you use your laptop for work the whole day, this will happen multiple times daily. **It will break it**. + +## Solution + +### For MacBooks (magsafe): + +Put a piece of paper/cloth on your middle connector of your MacBook Magsafe charger **whenever your laptop plugged in for long periods**. + +
+ +![](images/howto.jpg) + +
+ +How-To protect the battery with Magsafe chargers + +
+ +
+ +**Update:** I tried new methods, the one that seems the most practical is to use a little piece of tape that you can bend in front of the connector when needed. + +##### Updated method with tape + +Put a piece of tape on your middle connector. For simplicity you can just cover 3 pins and leave the 2 outside pins (does not matter which side) free. +Now you can easily switch between loading the battery or just working on power **whenever your laptop plugged in for long periods**. + +
+ +![](images/howto-1.jpg) + +
+ +Same method, just with tape. Much easier to use. + +
+ +
+ +
+ +![](images/status.jpg) + +
+ +Status of battery after modification + +
+ +
+ +This will prevent your laptop from using **and** charging your battery while using it for a whole day. +**Credits for the hack**: [https://superuser.com/a/1130375](https://superuser.com/a/1130375) + +### Laptops with removable batteries: + +Simply remove the battery when using laptop for long periods. This will prevent the heat of the laptop being transferred to the battery and it won't charge over and over again to 100%. + +#### Sources + +- [https://batteryuniversity.com/index.php/learn/article/charging_lithium_ion_batteries](https://batteryuniversity.com/index.php/learn/article/charging_lithium_ion_batteries) +- [https://batteryuniversity.com/learn/article/bu_808b_what_causes_li_ion_to_die](https://batteryuniversity.com/learn/article/bu_808b_what_causes_li_ion_to_die) +- [https://batteryuniversity.com/index.php/learn/article/how_to_prolong_lithium_based_batteries](https://batteryuniversity.com/index.php/learn/article/how_to_prolong_lithium_based_batteries) +- [https://batteryuniversity.com/index.php/learn/article/do_and_dont_battery_table](https://batteryuniversity.com/index.php/learn/article/do_and_dont_battery_table) +- [https://www.electricbike.com/how-to-make-lithium-battery-last/](https://www.electricbike.com/how-to-make-lithium-battery-last/) +- [https://www.androidauthority.com/maximize-battery-life-882395/](https://www.androidauthority.com/maximize-battery-life-882395/) diff --git a/src/content/blog/how-to-bring-your-neural-network-to-the-web.md b/src/content/blog/how-to-bring-your-neural-network-to-the-web.md new file mode 100644 index 0000000..a94ef80 --- /dev/null +++ b/src/content/blog/how-to-bring-your-neural-network-to-the-web.md @@ -0,0 +1,340 @@ +--- +title: 'How to bring your neural network to the web' +date: '2020-02-10' +categories: + - 'coding' +tags: + - 'ai' + - 'keras' + - 'machine-learning' + - 'tensorflow' +coverImage: './images/natasha-connell-byp5TTxUbL0-unsplash-scaled-1.jpg' +--- + +Artificial intelligence, neural networks, machine learning. I don't know which of them is the bigger buzzword. If we look past the hype there are some actually very interesting use cases for machine learning in the browser. + +**For the lazy that simply what to just to the source code** +[Here is the git repo](https://github.com/cupcakearmy/mnist) for you :) +**Or simply go to the [finished website](https://mnist.nicco.io/)** + +Today we will look on how to train a simple mnist digit recogniser and then export it into a website where we then can see it in action. Therefore this article will be split into three parts + +1. Training +2. Export & import the pre-trained model into a website +3. Build a simple website where we can use the model. + +Also I am not going to explain what machine learning is, as there are enough guides, videos, podcasts, ... that already do a much better job than I could and would be outside the scope of this article. + +
+ +![](images/natasha-connell-byp5TTxUbL0-unsplash-scaled-1.jpg) + +
+ +Photo byΒ [Natasha Connell](https://unsplash.com/@natcon773?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/s/photos/brain?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +So the first thing we need to understand is that we will not train the model in the browser. That is a job for GPUs and the goal here is only to use a pre-trained model inside of the browser. Training is a much more resource intensive task than simply using the net. + +## Training the model + +So, the first step is to actually have a model. I will do this in tensorflow 2.0 using the now included keras api. This means Python πŸŽ‰ + +The code below is basically an adapted version of the [keras hello world example](https://keras.io/examples/mnist_cnn/). +If you want to run the code yourself (which you should!) simply head over to [Google Colab](https://colab.research.google.com), create a new file and just paste the code. There you can run it for free on GPUs which is pretty dope! + +``` +from tensorflow.keras.datasets import mnist +from tensorflow.keras.models import Sequential +from tensorflow.keras.layers import Dense, Dropout, Flatten +from tensorflow.keras.layers import Conv2D, MaxPooling2D +from tensorflow.keras.utils import to_categorical + +(x_train, y_train), (x_test, y_test) = mnist.load_data() + +# Reshaping for channels_last (tensorflow) with one channel +size = 28 +print(x_train.shape, x_test.shape) +x_train = x_train.reshape(len(x_train), size, size, 1).astype('float32') +x_test = x_test.reshape(len(x_test), size, size, 1).astype('float32') +print(x_train.shape, x_test.shape) + +# Normalize +upper = max(x_train.max(), x_test.max()) +lower = min(x_train.min(), x_test.min()) +print(f'Max: {upper} Min: {lower}') +x_train /= upper +x_test /= upper + +total_classes = 10 +y_train = to_categorical(y_train, total_classes) +y_test = to_categorical(y_test, total_classes) + +# Make the model +model = Sequential() +model.add(Conv2D(64, (3, 3), activation='relu', input_shape=(size,size, 1), data_format='channels_last')) +model.add(Conv2D(32, (3, 3), activation='relu')) +model.add(MaxPooling2D(pool_size=(2, 2))) +model.add(Dropout(0.25)) +model.add(Flatten()) +model.add(Dense(128, activation='relu')) +model.add(Dropout(0.5)) +model.add(Dense(total_classes, activation='softmax')) + +model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) + +# Train +model.fit(x_train, y_train, + batch_size=32, + epochs=12, + verbose=True) + +score = model.evaluate(x_test, y_test, verbose=0) +print('Test loss:', score[0]) +print('Test accuracy:', score[1]) +``` + +We can run this and we will get a pretty good accuracy. The MNIST dataset ist not very hard to train. + +## Export the model + +Now the conventional way to save a model is to use the `model.save("model.h5")` method provided by keras. This uses the h5 file format. +Unfortunately this is not compatible with tensorflow-js. So we need another way. + +There is a package called tensorflowjs for python (confusing right? πŸ˜…) that provides the functionality we need + +``` +import tensorflowjs as tfjs + +tfjs.converters.save_keras_model(model, './js') +``` + +It save the model data inside the `./js` folder ready to be used. +Inside there you will find a `model.json` that basically describes the structure of the model and something like `group1-shard1of1.bin` that contains the fitted weights. + +## Import the model + +Now we are ready to import that. First we need to install the `@tensorflow/tfjs` package. + +``` +import * as tf from '@tensorflow/tfjs'; + +let model + +tf.loadLayersModel('/model.json').then(m => { + model = m +}) +``` + +Ok how do I use that now? + +``` +const tensor = tf.tensor(new Uint8Array(ourData), [1, 28, 28, 1]) +const prediction = model.predict(tensor) +``` + +**What is happening here?** +In order to predict a value we first need a tensor (vector) the same shape as our original input with which we trained the model with. In our case that is 1x28x28x1. +Also we will convert our pixel data into a `Uint8Array`. + +## Using the canvas element to draw and predict numbers + +I'm not gonna talk about what bundler, etc. I'm using. If you interested simply have a look at the [git repo](https://github.com/cupcakearmy/mnist). + +First lets write some basic html for the skeleton of our page. + +``` + + + + + + + +
+

MNIST (Pretrained)

+ +
+ +
+ +
+

+ +

source code

+
+
+ + + + + + +``` + +Next we need come short code for drawing on a canvas. +The code is adapted from [this stackoverflow answer](https://stackoverflow.com/a/8398189) and reduced to the only the basics we need. + +In essence it's a canvas that listens on our mouse events and fills the pixels with black. Nothing more. + +``` +/* jslint esversion: 6, asi: true */ + +var canvas, ctx, flag = false, + prevX = 0, + currX = 0, + prevY = 0, + currY = 0, + dot_flag = false; + +var x = "black", + y = 2; + +function init() { + canvas = document.getElementById('can'); + ctx = canvas.getContext("2d"); + w = canvas.width; + h = canvas.height; + + canvas.addEventListener("mousemove", function (e) { + findxy('move', e) + }, false); + canvas.addEventListener("mousedown", function (e) { + findxy('down', e) + }, false); + canvas.addEventListener("mouseup", function (e) { + findxy('up', e) + }, false); + canvas.addEventListener("mouseout", function (e) { + findxy('out', e) + }, false); + + + window.document.getElementById('clear').addEventListener('click', erase) +} + +function draw() { + ctx.beginPath(); + ctx.moveTo(prevX, prevY); + ctx.lineTo(currX, currY); + ctx.strokeStyle = x; + ctx.lineWidth = y; + ctx.stroke(); + ctx.closePath(); +} + +function erase() { + ctx.clearRect(0, 0, w, h); +} + +function findxy(res, e) { + if (res == 'down') { + prevX = currX; + prevY = currY; + currX = e.clientX - canvas.offsetLeft; + currY = e.clientY - canvas.offsetTop; + + flag = true; + dot_flag = true; + if (dot_flag) { + ctx.beginPath(); + ctx.fillStyle = x; + ctx.fillRect(currX, currY, 2, 2); + ctx.closePath(); + dot_flag = false; + } + } + if (res == 'up' || res == "out") { + flag = false; + } + if (res == 'move') { + if (flag) { + prevX = currX; + prevY = currY; + currX = e.clientX - canvas.offsetLeft; + currY = e.clientY - canvas.offsetTop; + draw(); + } + } +} + +init() +``` + +And not the glue to put this together is the piece of code that listens on the "test" button. + +``` +import * as tf from '@tensorflow/tfjs'; + +let model + +tf.loadLayersModel('/model.json').then(m => { + model = m +}) + +window.document.getElementById('test').addEventListener('click', async () => { + const canvas = window.document.querySelector('canvas') + + const { data, width, height } = canvas.getContext('2d').getImageData(0, 0, 28, 28) + + const tensor = tf.tensor(new Uint8Array(data.filter((_, i) => i % 4 === 3)), [1, 28, 28, 1]) + const prediction = model.predict(tensor) + const result = await prediction.data() + const guessed = result.indexOf(1) + console.log(guessed) + window.document.querySelector('#result').innerText = guessed +}) +``` + +Here we need to explain a few things. +`canvas.getContext('2d').getImageData(0, 0, 28, 28)` simply returns a flattened array of the pixels from the point (0,0) to (28,28). + +Then, instead of simply passing the data to the tensor. we need to do some magic with `data.filter` in order to get only every 3rd pixel. This is because our canvas has 3 channels + 1 alpha, but we only need to know if the pixel is black or not. We do this by simply filtering for the index mod 4 + +``` +data.filter((_, i) => i % 4 === 3) +``` + +Lastly we need to interpret the result. `prediction.data()` return an array with 10 items. Because we have trained it that way that we only have 10 possible outcomes. 10 Digits right? +Well in that case we simply search in which position in the array we have a 1 and the index is out solution. +We search for a 1 because we only have floats from 0 to 1. So 1 is the maximum. + +I hope this helped you understand the process better. It was pretty confusing at first for me too 😬 diff --git a/src/content/blog/how-to-search-in-the-jam.md b/src/content/blog/how-to-search-in-the-jam.md new file mode 100644 index 0000000..a2d405a --- /dev/null +++ b/src/content/blog/how-to-search-in-the-jam.md @@ -0,0 +1,222 @@ +--- +title: "How to search in the JAM" +date: "2020-12-06" +categories: + - "coding" +tags: + - "jam-stack" + - "lunr" + - "search" + - "svelte" +--- + +So a lot (me included) now are building JAM stack landing pages, shops, full-stack apps, etc. and while you can have a backend of course not all of them have. For those who don't: **How do we search?** + +So there is the obvious [Google Programmable Search Engine](https://programmablesearchengine.google.com/about/) but that looks bad and it not really customizable. The results are very good, it's google after all. However for those who want something more custom: Here's one way how. + +A working example can found right here [nicco.io/search](https://nicco.io/search) πŸ˜‰ +We will look at the following: + +1. How to implement the search +2. Search Accuracy & Precision +3. Performance & Size + +
+ +![Telescope](images/uriel-soberanes-gCeH4z9m7bg-unsplash-991x1024.jpg) + +
+ +Photo by [Uriel Soberanes](https://unsplash.com/@soberanes?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/telescope?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +We can't rely on a backend as discussed above, so the magic will happen at build time, like everything in the JAM-verse. + +I've decided to go with the free and open source [lunr.js](https://lunrjs.com/) which is a simple but still quite powerful search engine that can run in the client. + +``` +const idx = lunr(function () { + this.ref('name') + this.field('text') + this.add({ name: 'A', text: 'Lorem...' }) +}) +const results = idx.search('Lor*') +``` + +The first question that probably will pop out in your head is: "How will lunr be able to know what is on our website?" Here is where our work begins. + +## Roadmap + +1. Aggregate all the data of your site +2. Prebuild the index and make it available as static JSON +3. Load `lunr.js` into your site and start searching + +## Preparing the Index + +So I'm using [Sapper](https://sapper.svelte.dev/) for this blog so the examples will be based on it, but the same principle applies to all JAM tech. + +First we need to aggregate all our data. In my case this means all the single pages, blog entries, projects and works. So I created a `/src/routes/search.json.js` file and got to work. + +``` +import lunr from 'lunr' + +import { getAll } from '../lib/wp' + +function removeHTML(s) { + return s.replace(/<.*?>|\s+|&#\d+;/g, ' ').trim() +} + +async function convertForIdx(type, fields = []) { + // Load the data from Wordpress + const items = await getAll(type) + // Map only the fields we need and are relevant + const defaults = ['title', 'content', 'slug'] + return items.map((item) => ({ + url: `${item.type}/${item.slug}`, + data: [...defaults, ...fields].map((field) => removeHTML(item[field])).join(' '), + })) +} + +export async function get(req, res) { + const all = await Promise.all([ + convertForIdx('projects', ['description']), + convertForIdx('pages'), + convertForIdx('posts'), + convertForIdx('works', ['role']), + ]) + + const idx = lunr(function () { + this.ref('url') + this.field('data') + + all.flat().forEach((doc) => this.add(doc)) + }) + res.setHeader('Content-Type', 'application/json') + res.end(JSON.stringify(idx)) +} +``` + +First I get all the data from the Wordpress backend and for each item I select at least the `title` and `content` as I want them to be searchable. Then we remove any html tags with a dirty regexp and finally we build the index. + +When we call `JSON.stringify(idx)` the precomputed index will be serialized to JSON. Otherwise every client would had to compute that on their CPU, wasting cycles and possibly battery. We don't want that. + +Now I have the "search model" ready. You can have a look: [nicco.io/search.json](https://nicco.io/search.json) + +## Integrating the search + +It's time to integrate the search into the actual website πŸš€ + +``` + + + + + +
    + {#each results as result (result.ref)} + + {/each} +
+``` + +The first thing we do is load our preloaded `/search.json` and loading into an instance of `lunr`. This only need to happen once, once the index is loaded we ready to go. + +``` +const idx = lunr.Index.load(prebuilt) +``` + +For the searching itself `lunr` has quite a [few options](https://lunrjs.com/guides/searching.html). The most relevant for me where the wildcard and fuzzy search. While wildcard is good for when we don't have completed a word yet, fuzzy helps us with typos. + +``` +const fuzzy = idx.search(needle + '~1') // foo~1 +``` + +While not explicitly said in the docs I'm guessing they use the [Levenshtein Distance](https://en.wikipedia.org/wiki/Levenshtein_distance), which means `~1` will replace at most 1 char. + +``` +const wildcard = idx.search(needle + '*') // fo* +``` + +Wildcard are straight forward. `lunr` supports any kind: `*oo`, `f*o` and `fo*`. + +The result is an array with the `ref` field so you can find the related item and a `score`. They are already sorted by score, so basically you just need to write a for loop. + +## Search Quality + +Now the accuracy and precision are of course on par with Google, but way good enough for a blog or a smaller site. However in 1h you can add search to your JAM site without much work and you stay google free. +Also this approach gives you all the artistic liberties over the design. + +## Performance & Size + +Since we are prebuilding and packaging the whole site into one big `JSON` file it's worth taking a look at the size of the index. + +For this I took the [Iliad by Homer](https://gutenberg.org/ebooks/6130) and slitted it up into different amount of pieces to simulate the amount of pages. At the same tame, the more pieces, the smaller the single content on one "page". + +Please not that it's ~1mb of plain text so it's quite a lot. +You can get the source code for the "test" [here](https://gist.github.com/cupcakearmy/242b54ee6b1a914896390c91846aa4d4). + +### Variable size documents + +
+ +![](https://api.nicco.io/wp-content/uploads/2020/12/Lunr-Index-Size-Compresion.svg) + +
+ +Graph of Lunr Index size + +
+ +
+ +As you can see, with `1000` each around `1.15k` in size we end up with a compressed size of `563 KiB` which starts to get big. + +### A more real example + +Here is an example where each document is around `10k` in size. Roughly double the text amount needed for this blog post. Then we add an ever increasing amount of documents to the index and watch it grow. + +
+ +![](https://api.nicco.io/wp-content/uploads/2020/12/Lunr.js-Index-Size-10k-Document-Size.svg) + +
+ +Chart of 10k sized documents building the index. + +
+ +
+ +The results are very different of course. Please note the the second graph has a logarithmic scale! If we compare the compressed size at `500` documents we have `494kb @2.3k/doc` vs `1.09MiB @10k/doc`. Basically double, which is not that bad if we consider that the documents are around 5 times bigger. + +Hope you enjoyed and learned something, take care ❀️ diff --git a/src/content/blog/images/Battery-Charge-Voltage-vs-Time.png b/src/content/blog/images/Battery-Charge-Voltage-vs-Time.png new file mode 100644 index 0000000..667e56f --- /dev/null +++ b/src/content/blog/images/Battery-Charge-Voltage-vs-Time.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54ef9ac3fb65009c53d55f103eadb1f6c1b9ac9cb93019f161d5c0d842576e61 +size 191614 diff --git a/src/content/blog/images/DST-cycles-web2.jpg b/src/content/blog/images/DST-cycles-web2.jpg new file mode 100644 index 0000000..9c55921 --- /dev/null +++ b/src/content/blog/images/DST-cycles-web2.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee60b8040150d154afd21faf3112421b9b43cacb0626a5b4c53c0590c69626a0 +size 151085 diff --git a/src/content/blog/images/IMG_0160-1024x436.jpeg b/src/content/blog/images/IMG_0160-1024x436.jpeg new file mode 100644 index 0000000..1f5f2ca --- /dev/null +++ b/src/content/blog/images/IMG_0160-1024x436.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01d67cb02737ac3cbfa236c4fed9fa4fc053a495a1d64d3d3aa44a751d1ca73e +size 80573 diff --git a/src/content/blog/images/IMG_0160.jpeg b/src/content/blog/images/IMG_0160.jpeg new file mode 100644 index 0000000..84a46a1 --- /dev/null +++ b/src/content/blog/images/IMG_0160.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:767f3552e0f0244b006c0bcb5a8d3c39b2fb83d1eb008cd5abb6d8c8012c11da +size 182432 diff --git a/src/content/blog/images/IMG_1709.jpeg b/src/content/blog/images/IMG_1709.jpeg new file mode 100644 index 0000000..f7542d2 --- /dev/null +++ b/src/content/blog/images/IMG_1709.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f43dfc65d7f710d7cfe7f21d168c5142c450b95b363e23f340d31135676bedc4 +size 33825 diff --git a/src/content/blog/images/IMG_1710.jpeg b/src/content/blog/images/IMG_1710.jpeg new file mode 100644 index 0000000..c1776a7 --- /dev/null +++ b/src/content/blog/images/IMG_1710.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e65961b86eb1febbe48eb9292c9a54eebdb61314a7b1223d7c9112423477dcf +size 175459 diff --git a/src/content/blog/images/IMG_1710.png b/src/content/blog/images/IMG_1710.png new file mode 100644 index 0000000..9dedb9b --- /dev/null +++ b/src/content/blog/images/IMG_1710.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e83975d7ec576ef6ca6c4fc7a007fd82d8066cdfc42ee097527f989ccf025756 +size 2027604 diff --git a/src/content/blog/images/IMG_1711-653x1024.jpeg b/src/content/blog/images/IMG_1711-653x1024.jpeg new file mode 100644 index 0000000..46e2404 --- /dev/null +++ b/src/content/blog/images/IMG_1711-653x1024.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f831b45aa91bc87f6882e64a702a8a3b6224a1f6a4bf33f8c21e46053409eed +size 68828 diff --git a/src/content/blog/images/IMG_1711.jpeg b/src/content/blog/images/IMG_1711.jpeg new file mode 100644 index 0000000..0030521 --- /dev/null +++ b/src/content/blog/images/IMG_1711.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1dfeb6c5d59b355b38a50a0888b178e9fa9fa93bd0d26dd94ffd87ecbc0fae3 +size 136338 diff --git a/src/content/blog/images/Screenshot-2020-01-29-at-14.55.28.png b/src/content/blog/images/Screenshot-2020-01-29-at-14.55.28.png new file mode 100644 index 0000000..25379a0 --- /dev/null +++ b/src/content/blog/images/Screenshot-2020-01-29-at-14.55.28.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4a5b54dfff9307f3460189e6e80eedd6c80ee64eb3ae25bf5c7df3c3cccf882 +size 56274 diff --git a/src/content/blog/images/Screenshot-2020-01-29-at-14.57.05.png b/src/content/blog/images/Screenshot-2020-01-29-at-14.57.05.png new file mode 100644 index 0000000..b20541b --- /dev/null +++ b/src/content/blog/images/Screenshot-2020-01-29-at-14.57.05.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0f241b1da4e817d415f8984ce54830276b0631042b31815a5569dd6937eb2ad +size 258865 diff --git a/src/content/blog/images/Screenshot-2020-04-11-at-23.25.48.png b/src/content/blog/images/Screenshot-2020-04-11-at-23.25.48.png new file mode 100644 index 0000000..e65fa9a --- /dev/null +++ b/src/content/blog/images/Screenshot-2020-04-11-at-23.25.48.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b5d821660053d05743eb6439d652e77a3d4699a45884a9bce88169835c55c8a +size 564881 diff --git a/src/content/blog/images/Screenshot-2021-01-28-at-12.12.59.png b/src/content/blog/images/Screenshot-2021-01-28-at-12.12.59.png new file mode 100644 index 0000000..51094e4 --- /dev/null +++ b/src/content/blog/images/Screenshot-2021-01-28-at-12.12.59.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5db61551895b9969afdb1356560f03c1918faca5e68169b6ac2d200ce700ce90 +size 73938 diff --git a/src/content/blog/images/Screenshot-2021-01-28-at-12.14.03.png b/src/content/blog/images/Screenshot-2021-01-28-at-12.14.03.png new file mode 100644 index 0000000..2fa8b7c --- /dev/null +++ b/src/content/blog/images/Screenshot-2021-01-28-at-12.14.03.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88c62af32b444a6928c85e8f31cad2f4bef8ebed33893b0a28894d6f87593599 +size 42234 diff --git a/src/content/blog/images/Screenshot-2021-03-23-at-10.20.32.png b/src/content/blog/images/Screenshot-2021-03-23-at-10.20.32.png new file mode 100644 index 0000000..c16fd32 --- /dev/null +++ b/src/content/blog/images/Screenshot-2021-03-23-at-10.20.32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb058e043414fe65d83876644ed84a7e2e336ad8b1b4d771062db8494c036e79 +size 101970 diff --git a/src/content/blog/images/Screenshot-2021-03-23-at-10.58.31-1-1024x325.png b/src/content/blog/images/Screenshot-2021-03-23-at-10.58.31-1-1024x325.png new file mode 100644 index 0000000..0cfa995 --- /dev/null +++ b/src/content/blog/images/Screenshot-2021-03-23-at-10.58.31-1-1024x325.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ca0617aa7989fc9540df159acfe986d2aff6d8d6060f56747ed983b5d5be5fb +size 29477 diff --git a/src/content/blog/images/Screenshot-2021-03-23-at-10.58.31-1.png b/src/content/blog/images/Screenshot-2021-03-23-at-10.58.31-1.png new file mode 100644 index 0000000..0a2a354 --- /dev/null +++ b/src/content/blog/images/Screenshot-2021-03-23-at-10.58.31-1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b70f8cacc7eaa9ec3127c96cf3d3fecddda61a35313b8fd6f42622d1f2983858 +size 50254 diff --git a/src/content/blog/images/Screenshot-2021-03-23-at-10.58.31.png b/src/content/blog/images/Screenshot-2021-03-23-at-10.58.31.png new file mode 100644 index 0000000..0a2a354 --- /dev/null +++ b/src/content/blog/images/Screenshot-2021-03-23-at-10.58.31.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b70f8cacc7eaa9ec3127c96cf3d3fecddda61a35313b8fd6f42622d1f2983858 +size 50254 diff --git a/src/content/blog/images/alessandra-caretto-cAY9X4rPG3g-unsplash.jpg b/src/content/blog/images/alessandra-caretto-cAY9X4rPG3g-unsplash.jpg new file mode 100644 index 0000000..505ff0c --- /dev/null +++ b/src/content/blog/images/alessandra-caretto-cAY9X4rPG3g-unsplash.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:641d3e8a3529e6e026773d16bc105b71a0ae27d8361faada37d52551ef198a0f +size 688323 diff --git a/src/content/blog/images/alina-grubnyak-1254785-unsplash-1024x683.jpg b/src/content/blog/images/alina-grubnyak-1254785-unsplash-1024x683.jpg new file mode 100644 index 0000000..080f025 --- /dev/null +++ b/src/content/blog/images/alina-grubnyak-1254785-unsplash-1024x683.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6b4a543a812755c5b899512478ce01fba1baea6502cffec3f9f46dc924fab30 +size 178827 diff --git a/src/content/blog/images/alina-grubnyak-1254785-unsplash-scaled.jpg b/src/content/blog/images/alina-grubnyak-1254785-unsplash-scaled.jpg new file mode 100644 index 0000000..6f267c8 --- /dev/null +++ b/src/content/blog/images/alina-grubnyak-1254785-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef1c569cdcf9851539f06176d0c9e0debc52875976ff330b1cc6971572fe1404 +size 665199 diff --git a/src/content/blog/images/amador-loureiro-BVyNlchWqzs-unsplash-1024x685.jpg b/src/content/blog/images/amador-loureiro-BVyNlchWqzs-unsplash-1024x685.jpg new file mode 100644 index 0000000..7449c47 --- /dev/null +++ b/src/content/blog/images/amador-loureiro-BVyNlchWqzs-unsplash-1024x685.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18e276ce5aa34268120c268de0f098dc07b0c0bb095d39479d3341af472acf7f +size 180270 diff --git a/src/content/blog/images/amador-loureiro-BVyNlchWqzs-unsplash-scaled.jpg b/src/content/blog/images/amador-loureiro-BVyNlchWqzs-unsplash-scaled.jpg new file mode 100644 index 0000000..5d6a70a --- /dev/null +++ b/src/content/blog/images/amador-loureiro-BVyNlchWqzs-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:641e531b6383c8552a05586e2ac44c53718c0b6130d72f23dcb8edf74ee4b3dd +size 794403 diff --git a/src/content/blog/images/arseny-togulev-1513013-unsplash-1024x576.jpg b/src/content/blog/images/arseny-togulev-1513013-unsplash-1024x576.jpg new file mode 100644 index 0000000..479e959 --- /dev/null +++ b/src/content/blog/images/arseny-togulev-1513013-unsplash-1024x576.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9621fd53443f20b7811c86005dc1c9fc4858dddd4a04c4b9df977592b1a58f04 +size 67338 diff --git a/src/content/blog/images/arseny-togulev-1513013-unsplash-scaled.jpg b/src/content/blog/images/arseny-togulev-1513013-unsplash-scaled.jpg new file mode 100644 index 0000000..722c2f1 --- /dev/null +++ b/src/content/blog/images/arseny-togulev-1513013-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f1402d168936c5ef8b97bc4f30afe3d4bae64136feb8d85b8e85bc15d3810d3 +size 287558 diff --git a/src/content/blog/images/asoggetti-418839-unsplash-1024x684.jpg b/src/content/blog/images/asoggetti-418839-unsplash-1024x684.jpg new file mode 100644 index 0000000..7226396 --- /dev/null +++ b/src/content/blog/images/asoggetti-418839-unsplash-1024x684.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5794d60199ad3f80242f5291b56e98d5d8278bde2d0ed11e3c001017140f0d6e +size 30457 diff --git a/src/content/blog/images/asoggetti-418839-unsplash-scaled.jpg b/src/content/blog/images/asoggetti-418839-unsplash-scaled.jpg new file mode 100644 index 0000000..7e79889 --- /dev/null +++ b/src/content/blog/images/asoggetti-418839-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40588829e34a45f71777c36e1dfdc9078bdf88d3196051034d20249d3a0ea539 +size 104152 diff --git a/src/content/blog/images/auth-sequence-auth-code-pkce-1024x833.png b/src/content/blog/images/auth-sequence-auth-code-pkce-1024x833.png new file mode 100644 index 0000000..005a1fb --- /dev/null +++ b/src/content/blog/images/auth-sequence-auth-code-pkce-1024x833.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0fa3d4bb042056f6e9adfdf0829f603b4b5771c7a724b862fe275b57924ca23 +size 98883 diff --git a/src/content/blog/images/auth-sequence-auth-code-pkce.png b/src/content/blog/images/auth-sequence-auth-code-pkce.png new file mode 100644 index 0000000..e409536 --- /dev/null +++ b/src/content/blog/images/auth-sequence-auth-code-pkce.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e7b72e16d3f5bcd368eb3b39078e32d0ef904023b7984f32fa2d0e364109479 +size 43477 diff --git a/src/content/blog/images/brett-jordan-qUp3bejuzs-unsplash-scaled.jpg b/src/content/blog/images/brett-jordan-qUp3bejuzs-unsplash-scaled.jpg new file mode 100644 index 0000000..66214de --- /dev/null +++ b/src/content/blog/images/brett-jordan-qUp3bejuzs-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48603b5e68e25cce1593bf8578f5cc882c1612c213d0c3041312f018087a9a78 +size 191775 diff --git a/src/content/blog/images/cards-1024x567.jpg b/src/content/blog/images/cards-1024x567.jpg new file mode 100644 index 0000000..981f973 --- /dev/null +++ b/src/content/blog/images/cards-1024x567.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a3ed4f673a36c253f694b9590052125c52f2dfdecfa4cce27df5fe33b7f380d +size 84760 diff --git a/src/content/blog/images/cards-scaled.jpg b/src/content/blog/images/cards-scaled.jpg new file mode 100644 index 0000000..c2ce175 --- /dev/null +++ b/src/content/blog/images/cards-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebc09fc3389fdd05f9d559692c3d24eb7bd99adba36cabfdcf707975670c2474 +size 483287 diff --git a/src/content/blog/images/chris-barbalis-Vvvl-mbboKk-unsplash-scaled.jpg b/src/content/blog/images/chris-barbalis-Vvvl-mbboKk-unsplash-scaled.jpg new file mode 100644 index 0000000..2d991e9 --- /dev/null +++ b/src/content/blog/images/chris-barbalis-Vvvl-mbboKk-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7856b748cc26c928c9fd5d43af23d43f72a6f11bf277685ef4b63ad8ada0528e +size 495251 diff --git a/src/content/blog/images/clayton-robbins-Ru09fQONJWo-unsplash-1024x683.jpg b/src/content/blog/images/clayton-robbins-Ru09fQONJWo-unsplash-1024x683.jpg new file mode 100644 index 0000000..8199d55 --- /dev/null +++ b/src/content/blog/images/clayton-robbins-Ru09fQONJWo-unsplash-1024x683.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:517f9417837a532be4d3830cdd1e5857582ff85299bd14ab6f8aa52278aed3bd +size 62672 diff --git a/src/content/blog/images/clayton-robbins-Ru09fQONJWo-unsplash-scaled.jpg b/src/content/blog/images/clayton-robbins-Ru09fQONJWo-unsplash-scaled.jpg new file mode 100644 index 0000000..73c5609 --- /dev/null +++ b/src/content/blog/images/clayton-robbins-Ru09fQONJWo-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:366666945890876b458c89ddfb94d4bab8e02f686a08b1d3bc04f0ed0e03e1cc +size 272668 diff --git a/src/content/blog/images/daniele-franchi-g2fJ7d7eKSM-unsplash-scaled.jpg b/src/content/blog/images/daniele-franchi-g2fJ7d7eKSM-unsplash-scaled.jpg new file mode 100644 index 0000000..e89d02e --- /dev/null +++ b/src/content/blog/images/daniele-franchi-g2fJ7d7eKSM-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cf645981a817db0c7c5e92723d1c1104390a12dc4f68fa8bb82127e98352020 +size 392117 diff --git a/src/content/blog/images/daniele-franchi-g2fJ7d7eKSM-unsplash.jpg b/src/content/blog/images/daniele-franchi-g2fJ7d7eKSM-unsplash.jpg new file mode 100644 index 0000000..66a23b6 --- /dev/null +++ b/src/content/blog/images/daniele-franchi-g2fJ7d7eKSM-unsplash.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3f3295dd8580c51c510f34b5c5252c56d77be75d1a93ce83583c6a4308e636f +size 4596995 diff --git a/src/content/blog/images/data.gif b/src/content/blog/images/data.gif new file mode 100644 index 0000000..41ba811 --- /dev/null +++ b/src/content/blog/images/data.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a326b48b5b9355bdef2a43fe5298559d45ad645f911519b9e398f981a1390e80 +size 318810 diff --git a/src/content/blog/images/davisco-5E5N49RWtbA-unsplash-3240x2160.jpg b/src/content/blog/images/davisco-5E5N49RWtbA-unsplash-3240x2160.jpg new file mode 100644 index 0000000..e11aaa4 --- /dev/null +++ b/src/content/blog/images/davisco-5E5N49RWtbA-unsplash-3240x2160.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1be1853bbb1056dd892fec171870e7bbe6108c4eab7309d13d594dd9b9c5ec28 +size 383858 diff --git a/src/content/blog/images/davisco-5E5N49RWtbA-unsplash-scaled-1.jpg b/src/content/blog/images/davisco-5E5N49RWtbA-unsplash-scaled-1.jpg new file mode 100644 index 0000000..26ed4bf --- /dev/null +++ b/src/content/blog/images/davisco-5E5N49RWtbA-unsplash-scaled-1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:418f648ed44631401d9b4887c951b1db562cb02d981258f410768284dccd4945 +size 160637 diff --git a/src/content/blog/images/franck-v-U3sOwViXhkY-unsplash-2880x2160.jpg b/src/content/blog/images/franck-v-U3sOwViXhkY-unsplash-2880x2160.jpg new file mode 100644 index 0000000..e11aaa4 --- /dev/null +++ b/src/content/blog/images/franck-v-U3sOwViXhkY-unsplash-2880x2160.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1be1853bbb1056dd892fec171870e7bbe6108c4eab7309d13d594dd9b9c5ec28 +size 383858 diff --git a/src/content/blog/images/franck-v-U3sOwViXhkY-unsplash-scaled-1.jpg b/src/content/blog/images/franck-v-U3sOwViXhkY-unsplash-scaled-1.jpg new file mode 100644 index 0000000..3611efb --- /dev/null +++ b/src/content/blog/images/franck-v-U3sOwViXhkY-unsplash-scaled-1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:394cb4b320efe0eaafc527569f2e04e57f25f6ca7ad70a6cc951af981012be2a +size 587956 diff --git a/src/content/blog/images/guillaume-bolduc-259596-unsplash-1024x741.jpg b/src/content/blog/images/guillaume-bolduc-259596-unsplash-1024x741.jpg new file mode 100644 index 0000000..0eb6fbe --- /dev/null +++ b/src/content/blog/images/guillaume-bolduc-259596-unsplash-1024x741.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:821eb3943493f35fbdb6881c48ab8c21870e1c7a11f11321e1bb7ee7ec0b6e16 +size 249493 diff --git a/src/content/blog/images/guillaume-bolduc-259596-unsplash-scaled.jpg b/src/content/blog/images/guillaume-bolduc-259596-unsplash-scaled.jpg new file mode 100644 index 0000000..cb3ff46 --- /dev/null +++ b/src/content/blog/images/guillaume-bolduc-259596-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b41d27d824c56267c2cb666d906c1bede2fdaa1de74f9957e26d502c50cbc2b +size 1115156 diff --git a/src/content/blog/images/howto-1.jpg b/src/content/blog/images/howto-1.jpg new file mode 100644 index 0000000..7399a91 --- /dev/null +++ b/src/content/blog/images/howto-1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39057dd4352501b6a8ad5aa62f28662cc0b1310fbbf03c5ad3ee67d0ca3abb9a +size 143009 diff --git a/src/content/blog/images/howto.jpg b/src/content/blog/images/howto.jpg new file mode 100644 index 0000000..535e2ab --- /dev/null +++ b/src/content/blog/images/howto.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8aaebdf6ec81ea65d61c9db30a93e0e6da65c06b874a614393d260ef135c3ee9 +size 170032 diff --git a/src/content/blog/images/hyeryi-sVk8nrCQ06g-unsplash-1024x683.jpg b/src/content/blog/images/hyeryi-sVk8nrCQ06g-unsplash-1024x683.jpg new file mode 100644 index 0000000..9f768e4 --- /dev/null +++ b/src/content/blog/images/hyeryi-sVk8nrCQ06g-unsplash-1024x683.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e8724656a1c04e896de528cc52d2418de73b5dbba1eb94062c826a212b18f19 +size 205615 diff --git a/src/content/blog/images/hyeryi-sVk8nrCQ06g-unsplash-scaled.jpg b/src/content/blog/images/hyeryi-sVk8nrCQ06g-unsplash-scaled.jpg new file mode 100644 index 0000000..03dc9dd --- /dev/null +++ b/src/content/blog/images/hyeryi-sVk8nrCQ06g-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:728917127520026b290af0348ef70dee6d36dbd3ec3f72b76d3c2b1fca3bdfdb +size 1205666 diff --git a/src/content/blog/images/israel-palacio-ImcUkZ72oUs-unsplash-scaled.jpg b/src/content/blog/images/israel-palacio-ImcUkZ72oUs-unsplash-scaled.jpg new file mode 100644 index 0000000..7e13457 --- /dev/null +++ b/src/content/blog/images/israel-palacio-ImcUkZ72oUs-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24f5c8a7e2fce080c7f9090f9db4f8ae5d2644f08eba8671da802bd76880e679 +size 523148 diff --git a/src/content/blog/images/jason-abdilla-jZWmw6007EY-unsplash-scaled.jpg b/src/content/blog/images/jason-abdilla-jZWmw6007EY-unsplash-scaled.jpg new file mode 100644 index 0000000..c2d3410 --- /dev/null +++ b/src/content/blog/images/jason-abdilla-jZWmw6007EY-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a7c6f461682a6a5d127655c2d67b90e1a1293c3fbe9bca763db8ab855e1c534 +size 907098 diff --git a/src/content/blog/images/jielin-chen-pKQIpxzq0ZQ-unsplash-e1562783699383-1024x484.jpg b/src/content/blog/images/jielin-chen-pKQIpxzq0ZQ-unsplash-e1562783699383-1024x484.jpg new file mode 100644 index 0000000..84cb6f3 --- /dev/null +++ b/src/content/blog/images/jielin-chen-pKQIpxzq0ZQ-unsplash-e1562783699383-1024x484.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eef5e387e8c930d6eff083bc6b460613253d1d808147515a0430bdbffac3ccfe +size 56082 diff --git a/src/content/blog/images/jielin-chen-pKQIpxzq0ZQ-unsplash-e1562783699383-scaled.jpg b/src/content/blog/images/jielin-chen-pKQIpxzq0ZQ-unsplash-e1562783699383-scaled.jpg new file mode 100644 index 0000000..6c3b3fa --- /dev/null +++ b/src/content/blog/images/jielin-chen-pKQIpxzq0ZQ-unsplash-e1562783699383-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82a2dfcd574fef01451a2e62c5efa1975445682c70588bca4e06f1421be4c24d +size 225187 diff --git a/src/content/blog/images/jonathan-chng-HgoKvtKpyHA-unsplash-3504x2160.jpg b/src/content/blog/images/jonathan-chng-HgoKvtKpyHA-unsplash-3504x2160.jpg new file mode 100644 index 0000000..e11aaa4 --- /dev/null +++ b/src/content/blog/images/jonathan-chng-HgoKvtKpyHA-unsplash-3504x2160.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1be1853bbb1056dd892fec171870e7bbe6108c4eab7309d13d594dd9b9c5ec28 +size 383858 diff --git a/src/content/blog/images/jonathan-chng-HgoKvtKpyHA-unsplash-scaled-1.jpg b/src/content/blog/images/jonathan-chng-HgoKvtKpyHA-unsplash-scaled-1.jpg new file mode 100644 index 0000000..8112d96 --- /dev/null +++ b/src/content/blog/images/jonathan-chng-HgoKvtKpyHA-unsplash-scaled-1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16f73c9118460e939f16070572407482c45a885a5f28163d3fe21a1ae63efc4f +size 547290 diff --git a/src/content/blog/images/luke-chesser-JKUTrJ4vK00-unsplash-1024x683.jpg b/src/content/blog/images/luke-chesser-JKUTrJ4vK00-unsplash-1024x683.jpg new file mode 100644 index 0000000..18c8b67 --- /dev/null +++ b/src/content/blog/images/luke-chesser-JKUTrJ4vK00-unsplash-1024x683.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0e78000dcffc29f4e60a546ff72c0b038165b6bb6ba6c2a75fc036628bd50a6 +size 75268 diff --git a/src/content/blog/images/luke-chesser-JKUTrJ4vK00-unsplash-scaled.jpg b/src/content/blog/images/luke-chesser-JKUTrJ4vK00-unsplash-scaled.jpg new file mode 100644 index 0000000..eaf64b5 --- /dev/null +++ b/src/content/blog/images/luke-chesser-JKUTrJ4vK00-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bdd3909cf6516e603c072d23f1ba8a9394dc29cfdaff787c0a6ecd3ce0083c99 +size 351535 diff --git a/src/content/blog/images/mark-autumns-Ssr26I0QWVY-unsplash-1024x683.jpg b/src/content/blog/images/mark-autumns-Ssr26I0QWVY-unsplash-1024x683.jpg new file mode 100644 index 0000000..79feb80 --- /dev/null +++ b/src/content/blog/images/mark-autumns-Ssr26I0QWVY-unsplash-1024x683.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:864ac4e4da80d30f45124e0f377e2453b843017f8442f8b2858f644d3076f1d6 +size 110652 diff --git a/src/content/blog/images/mark-autumns-Ssr26I0QWVY-unsplash-scaled.jpg b/src/content/blog/images/mark-autumns-Ssr26I0QWVY-unsplash-scaled.jpg new file mode 100644 index 0000000..a95df1e --- /dev/null +++ b/src/content/blog/images/mark-autumns-Ssr26I0QWVY-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:012f5cffade95b8952c6da835c5ec9badacce4ab222b8cf6a59ce2d62fb2fd4a +size 645501 diff --git a/src/content/blog/images/markus-spiske-8CWoXxaqGrs-unsplash-1024x683.jpg b/src/content/blog/images/markus-spiske-8CWoXxaqGrs-unsplash-1024x683.jpg new file mode 100644 index 0000000..e3da666 --- /dev/null +++ b/src/content/blog/images/markus-spiske-8CWoXxaqGrs-unsplash-1024x683.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:031b2833eaca905035d9bcee4462ab92d6388ccbeffde7ef775d792231726fbf +size 40933 diff --git a/src/content/blog/images/markus-spiske-8CWoXxaqGrs-unsplash-scaled.jpg b/src/content/blog/images/markus-spiske-8CWoXxaqGrs-unsplash-scaled.jpg new file mode 100644 index 0000000..ec48929 --- /dev/null +++ b/src/content/blog/images/markus-spiske-8CWoXxaqGrs-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:500b324d99022e3410afa467743fff186a85700462e66487c11f5b7c3722e9d1 +size 244788 diff --git a/src/content/blog/images/matt-artz-353210-unsplash-1024x780.jpg b/src/content/blog/images/matt-artz-353210-unsplash-1024x780.jpg new file mode 100644 index 0000000..dea01c0 --- /dev/null +++ b/src/content/blog/images/matt-artz-353210-unsplash-1024x780.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:846aa0ef3c0ad69b286514962c0cf1b9504f0fc74e975f592cfbe79f06ed5076 +size 40291 diff --git a/src/content/blog/images/matt-artz-353210-unsplash-scaled.jpg b/src/content/blog/images/matt-artz-353210-unsplash-scaled.jpg new file mode 100644 index 0000000..0b73d1a --- /dev/null +++ b/src/content/blog/images/matt-artz-353210-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:08eb7d31540d64f49de77452565c48015bc4da3e5753ba9ed93ad19e70eaef48 +size 190710 diff --git a/src/content/blog/images/meagan-carsience-QGnm_F_nd1E-unsplash1-1024x683.jpg b/src/content/blog/images/meagan-carsience-QGnm_F_nd1E-unsplash1-1024x683.jpg new file mode 100644 index 0000000..b915f1a --- /dev/null +++ b/src/content/blog/images/meagan-carsience-QGnm_F_nd1E-unsplash1-1024x683.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f77858db14fe890542ef1c18b112f144f4bdf5a1f9009cdcfa0f9f80eab70c55 +size 19395 diff --git a/src/content/blog/images/meagan-carsience-QGnm_F_nd1E-unsplash1.jpg b/src/content/blog/images/meagan-carsience-QGnm_F_nd1E-unsplash1.jpg new file mode 100644 index 0000000..f73d30b --- /dev/null +++ b/src/content/blog/images/meagan-carsience-QGnm_F_nd1E-unsplash1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4def0c84923defe516c9d1c7e4be767ff6a7ecfdccf55eb674abf3b5144dbc06 +size 92471 diff --git a/src/content/blog/images/milkovi-kYlYwQze5vI-unsplash-1-scaled.jpg b/src/content/blog/images/milkovi-kYlYwQze5vI-unsplash-1-scaled.jpg new file mode 100644 index 0000000..68338e0 --- /dev/null +++ b/src/content/blog/images/milkovi-kYlYwQze5vI-unsplash-1-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7302bfcb09c4fcaa7cbc1b51254697333f3a0b46090809779bd4a7c49442454a +size 445129 diff --git a/src/content/blog/images/natasha-connell-byp5TTxUbL0-unsplash-2880x2160.jpg b/src/content/blog/images/natasha-connell-byp5TTxUbL0-unsplash-2880x2160.jpg new file mode 100644 index 0000000..e11aaa4 --- /dev/null +++ b/src/content/blog/images/natasha-connell-byp5TTxUbL0-unsplash-2880x2160.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1be1853bbb1056dd892fec171870e7bbe6108c4eab7309d13d594dd9b9c5ec28 +size 383858 diff --git a/src/content/blog/images/natasha-connell-byp5TTxUbL0-unsplash-scaled-1.jpg b/src/content/blog/images/natasha-connell-byp5TTxUbL0-unsplash-scaled-1.jpg new file mode 100644 index 0000000..4060fcd --- /dev/null +++ b/src/content/blog/images/natasha-connell-byp5TTxUbL0-unsplash-scaled-1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0160fa88ffe90a0f9a1846e178b4554db761855aea838973bc32eb700cd28af9 +size 247279 diff --git a/src/content/blog/images/noah-silliman-doBrZnp_wqA-unsplash.jpg b/src/content/blog/images/noah-silliman-doBrZnp_wqA-unsplash.jpg new file mode 100644 index 0000000..f45a4c6 --- /dev/null +++ b/src/content/blog/images/noah-silliman-doBrZnp_wqA-unsplash.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ff024e32cdb7fa6fe8624cc7c42f16a3b7c2981646ab0f01c6e81c6b56a02f3 +size 393856 diff --git a/src/content/blog/images/pawel-nolbert-xe-ss5Tg2mo-unsplash-1024x740.jpg b/src/content/blog/images/pawel-nolbert-xe-ss5Tg2mo-unsplash-1024x740.jpg new file mode 100644 index 0000000..db33e63 --- /dev/null +++ b/src/content/blog/images/pawel-nolbert-xe-ss5Tg2mo-unsplash-1024x740.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f03cb447948ec9253aa13c4049dce5d2ae2c663632d8bd1c0a4e813ac9e2f771 +size 67592 diff --git a/src/content/blog/images/pawel-nolbert-xe-ss5Tg2mo-unsplash.jpg b/src/content/blog/images/pawel-nolbert-xe-ss5Tg2mo-unsplash.jpg new file mode 100644 index 0000000..8082c68 --- /dev/null +++ b/src/content/blog/images/pawel-nolbert-xe-ss5Tg2mo-unsplash.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd058a3a94865f064a2cc5300267657d449d7cb787662b2ceb945ebe80dfafc7 +size 555132 diff --git a/src/content/blog/images/permissions.gif b/src/content/blog/images/permissions.gif new file mode 100644 index 0000000..6ab05cf --- /dev/null +++ b/src/content/blog/images/permissions.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc7bc17cdc583accf94e27eadb4bdb49c91e58c6add60074d24e6b71b1d35e19 +size 485939 diff --git a/src/content/blog/images/rayan-almuslem-1302778-unsplash-1024x683.jpg b/src/content/blog/images/rayan-almuslem-1302778-unsplash-1024x683.jpg new file mode 100644 index 0000000..62dcf02 --- /dev/null +++ b/src/content/blog/images/rayan-almuslem-1302778-unsplash-1024x683.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84f88b7455367e2ff8f61e1b0cf341437a0eea63aaa07acb3c3e68e7b766527c +size 50981 diff --git a/src/content/blog/images/rayan-almuslem-1302778-unsplash-scaled.jpg b/src/content/blog/images/rayan-almuslem-1302778-unsplash-scaled.jpg new file mode 100644 index 0000000..af876b7 --- /dev/null +++ b/src/content/blog/images/rayan-almuslem-1302778-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f1258ab1f0583e4cb7225f97c777685f0b35021e71ab9ce3ac838fbcaad714d +size 299888 diff --git a/src/content/blog/images/register-bot.jpg b/src/content/blog/images/register-bot.jpg new file mode 100644 index 0000000..b878cb4 --- /dev/null +++ b/src/content/blog/images/register-bot.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4aa44481d72090565e8664bdfbb506f833cabc9a0e1ac988807912d86dc61c71 +size 97464 diff --git a/src/content/blog/images/ruchindra-gunasekara-GK8x_XCcDZg-unsplash-scaled.jpg b/src/content/blog/images/ruchindra-gunasekara-GK8x_XCcDZg-unsplash-scaled.jpg new file mode 100644 index 0000000..63ac7f0 --- /dev/null +++ b/src/content/blog/images/ruchindra-gunasekara-GK8x_XCcDZg-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:979fc17abb2549a5038ff4d0c963fac8549667e4f7efcdcb4d91601a7cdddebd +size 838338 diff --git a/src/content/blog/images/ruchindra-gunasekara-GK8x_XCcDZg-unsplash.jpg b/src/content/blog/images/ruchindra-gunasekara-GK8x_XCcDZg-unsplash.jpg new file mode 100644 index 0000000..30e08ad --- /dev/null +++ b/src/content/blog/images/ruchindra-gunasekara-GK8x_XCcDZg-unsplash.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9a2b0d913d11a177032bf5c8148c43e64e97dcff8c0224645f82a8e5e7280c9 +size 4819801 diff --git a/src/content/blog/images/status.jpg b/src/content/blog/images/status.jpg new file mode 100644 index 0000000..9f20341 --- /dev/null +++ b/src/content/blog/images/status.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd832991f433a79e9794b29d80c170486d55187b565e1234f69e2cc141291042 +size 57217 diff --git a/src/content/blog/images/thomas-kelley-t20pc32VbrU-unsplash-3240x2160.jpg b/src/content/blog/images/thomas-kelley-t20pc32VbrU-unsplash-3240x2160.jpg new file mode 100644 index 0000000..e11aaa4 --- /dev/null +++ b/src/content/blog/images/thomas-kelley-t20pc32VbrU-unsplash-3240x2160.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1be1853bbb1056dd892fec171870e7bbe6108c4eab7309d13d594dd9b9c5ec28 +size 383858 diff --git a/src/content/blog/images/thomas-kelley-t20pc32VbrU-unsplash-scaled-1.jpg b/src/content/blog/images/thomas-kelley-t20pc32VbrU-unsplash-scaled-1.jpg new file mode 100644 index 0000000..8869909 --- /dev/null +++ b/src/content/blog/images/thomas-kelley-t20pc32VbrU-unsplash-scaled-1.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a2d827c72bad2602c6f1f9fa29fcb2471de06dd725feb338b247b034480da05 +size 773178 diff --git a/src/content/blog/images/tobias-fischer-PkbZahEG2Ng-unsplash-1024x497.jpg b/src/content/blog/images/tobias-fischer-PkbZahEG2Ng-unsplash-1024x497.jpg new file mode 100644 index 0000000..0e100bc --- /dev/null +++ b/src/content/blog/images/tobias-fischer-PkbZahEG2Ng-unsplash-1024x497.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c48da45eb028f51863c7ca093626e9270ee59ce3aa4807ffbe9ef676e5cfe50d +size 142735 diff --git a/src/content/blog/images/tobias-fischer-PkbZahEG2Ng-unsplash-scaled.jpg b/src/content/blog/images/tobias-fischer-PkbZahEG2Ng-unsplash-scaled.jpg new file mode 100644 index 0000000..705aa02 --- /dev/null +++ b/src/content/blog/images/tobias-fischer-PkbZahEG2Ng-unsplash-scaled.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e195c9ed36de6e74ff814f663d80387de82245685784e9c5b52e2831d860f09 +size 615029 diff --git a/src/content/blog/images/uriel-soberanes-gCeH4z9m7bg-unsplash-991x1024.jpg b/src/content/blog/images/uriel-soberanes-gCeH4z9m7bg-unsplash-991x1024.jpg new file mode 100644 index 0000000..f7f97d7 --- /dev/null +++ b/src/content/blog/images/uriel-soberanes-gCeH4z9m7bg-unsplash-991x1024.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:487107e00c930e0b37bce0312df5c9e1801bb8a41978f67d76c488e14f40ac20 +size 83157 diff --git a/src/content/blog/images/uriel-soberanes-gCeH4z9m7bg-unsplash.jpg b/src/content/blog/images/uriel-soberanes-gCeH4z9m7bg-unsplash.jpg new file mode 100644 index 0000000..c16b2b6 --- /dev/null +++ b/src/content/blog/images/uriel-soberanes-gCeH4z9m7bg-unsplash.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06f7370a7d558d0abef5b4b0213e574ae8de04416135fe37a4ca949d9c587b82 +size 905920 diff --git a/src/content/blog/leaving-nextcloud-from-heaven-to-the-depths-of-seafile.md b/src/content/blog/leaving-nextcloud-from-heaven-to-the-depths-of-seafile.md new file mode 100644 index 0000000..9f15dbd --- /dev/null +++ b/src/content/blog/leaving-nextcloud-from-heaven-to-the-depths-of-seafile.md @@ -0,0 +1,161 @@ +--- +title: 'Leaving Nextcloud: From heaven to the depths of Seafile' +date: '2021-03-23' +categories: + - 'general' +tags: + - 'migration' + - 'nextcloud' + - 'seafile' +coverImage: './images/pawel-nolbert-xe-ss5Tg2mo-unsplash.jpg' +--- + +Today I'll share and explain the motivations that lead me to leave Nextcloud and choose Seafile, while hopefully making the transition smooth for anyone that wants to follow along. Spoiler: Performance and stuff constantly breaking leads to a Seafile beta being way more stable and reliable that a Nextcloud "Production" release. + +Nextcloud is the the de-facto way to go for most self hosted Cloud / Dropbox / Google Drive. So a few years ago in my move to a more ownership driven approach to my data I setup a Nextcloud instance and was quite happy. They have an iOS app and a macOS sync client. +There are numerous of plug-ins that can accomplish anything from contacts syncing with WebDav to GSuite-Like online collaboration with the Collabora integration, a mail client and real time video conferences with Nextcloud Talk. For many (persons & businesses) this means a convenient place where all the tools are combined under one platform. + +## Where the issues started + +Trying to be everything at the same time comes at a cost. And that is generally an experience that at least in my experience never feels polished or finished. While the Nextcloud Plug-Ins are incredibly versatile and powerful they also leave room for segmentation and you will notice it. + +
+ +![](images/pawel-nolbert-xe-ss5Tg2mo-unsplash-1024x740.jpg) + +
+ +Cloud and Ocean + +
+ +
+ +### The permanent alpha + +That's what using Nextcloud feels like 75% of the time. I have no insight into the company behind the project but it feels like they are chasing a release cycle for the sake of paper launching unfinished features that compromise in terms of stability and polish. The thing that bothers me the most is that they are constantly marketed as "production ready" when they clearly had not nearly enough QA. + +2 years ago I tried to install Collabora for an organisation I'm involved with and the setup was everything but straight forward. Docs where limited and the answers buried either in a forum thread or github issue. After many web searches I got it to work, but the performance (at least when I tested) was not really usable. We ended up using Google Docs and Etherpad. + +Then there was the story with end to end encryption (E2EE) for files. This was a feature that was promoted quite extensively by Nextcloud and [released as stable](https://nextcloud.com/blog/production-ready-end-to-end-encryption-and-new-user-interface-arrive-with-nextcloud-desktop-client-3-0/) after many delays. So I followed the instructions on installing it and... [the sync clients broke down](https://github.com/nextcloud/desktop/issues/2593). What happened? Well Nextcloud released software as stable and production ready, but the most basic functionality was simply not ready and a lot of clients stopped syncing, mine included. + +A few weeks ago [Nextcloud 21](https://nextcloud.com/blog/nextcloud-hub-21-out-with-up-to-10x-better-performance-whiteboard-and-more-collaboration-features/) was released and boasted a "10x Speed improvement" which of course is an assumption that cannot hold it's grounds. But I was excited as even a "1.5x-2x" improvement would have been really substantial. I installed the new version and well... performance seemed untouched but as a new feature my UI was stuck in a permanent dark mode UI with unreadable text (see the files sizes). I don't run any custom theme or anything. Is this a complete deal breaker? Of course not, but it goes a long way to show how untested the whole software is if a stable release has unreadable text in the start page of your product. + +
+ +![](images/Screenshot-2021-03-23-at-10.58.31-1-1024x325.png) + +
+ +Screenshot of Nextcloud 21 with colour bugs + +
+ +
+ +In addition to the weird CSS bugs it also introduced a new bug where I have to frequently reload the web UI as it "cannot connect" to the server. Definetely production ready. + +### Performance + +The final issue is the performance as a whole. The web interface regularly takes around 5-10s to load for each action you perform. The only thing that is quite responsive is navigating through folders. It's a drag when you just quickly want to get stuff done and the actual work you have to do takes less than waiting for Nextcloud to serve you the website. It's simply put not fun to use it. + +There is also the issue that you need to spin up a second container for cron jobs like it's 2003 and that every update or so you manually have to go into the console to rebuild some indexes. I'll leave them unjudged as it maybe those are "Enterprise Features" which I don't understand. + +## Seafile to the rescue + +The last update (Nextcloud 21) was the point where I decided to jump ship as explained above. The question was: What options do I have? + +- Pydio +- Seafile + +Pydio reinvented itself with the launch of it's Cells product. However at the time of writing the macOS client did not seem ready and therefore was excluded. + +Seafile on the other hand just had the release of it's 8th version (still in beta afaik) and supports iOS and macOS. + +### Migration + +I had to migrate 2 things: Cal/CardDav for Calendar and Contacts and the files drive itself. +Spinning up a Seafile instance was a breeze as I host every single service with docker. + +``` +# .env +MYSQL_ROOT_PASSWORD=random +DB_HOST=db +DB_ROOT_PASSWD=random + +SEAFILE_ADMIN_EMAIL=me@example.com +SEAFILE_ADMIN_PASSWORD=a_very_secret_password +``` + +``` +version: "2.0" + +services: + db: + image: mariadb:10.5 + env_file: .env + volumes: + - ./data/db:/var/lib/mysql + + memcached: + image: memcached:1.5.6 + entrypoint: memcached -m 256 + + app: + image: seafileltd/seafile-mc:latest + env_file: .env + volumes: + - ./data/app:/shared + depends_on: + - db + - memcached +``` + +I then installed the macOS client and simply copied all the files over. Before actually copying the files I added a `seafile-ignore.txt` file in the root to exclude files from being uploaded to the server. Read more [here](https://help.seafile.com/syncing_client/excluding_files/). + +**Gotcha** +The ignore file can be tricky if you tread it like a `.gitignore` files. `.git/` would not exclude all those directories, only the root one. To exclude all `.git` directories you actually need to insert `*/.git/` into the `seafile-ignore.txt` file. + +The UI and the sync is incredibly fast, especially when compared to Nextcloud and is delightful to use. It has all the features you would expect: 2FA, user groups, quotas, link sharing (with support for expiration, password and upload only), files sharing between users, etc. It also features collaboration features like Nextcloud but I haven't tested them yet. Also it features an actual REST API with tokens that you can generate (as read-write or read only tokens). Another issue I had to battle with in the past with Nextcloud. + +The mobile app on iOS does everything you would expect it to do, including integrating with the native Files API. + +For Notability I use the backup feature that uploads backups as PDFs of my notes to a WebDav server. However Seafile disables the usage of WebDav for users with 2FA as it would be a vulnerability. As a solution I simply created a "Notability" user without 2FA and shared the folder i want to use as target with that user. Awesome! Now I have a scoped user that only has access to the notability backup folder without having access to the rest of my files. + +## CardDav/CalDav + +Since Seafile focuses only on the "Drive" component I had to migrate my contacts and calendars elsewhere. The way to go solution is Radicale and I was surprised to find that there is not official docker image? After a 2-min research I found the most popular docker image did not support Authentication? So I had to create my own. + +You can find my [Radicale docker image here](https://github.com/cupcakearmy/docker-radicale), maybe you find it useful. It supports bcrypt passwords and can be deployed with just the env variables for `USER` and `PASSWORD`. It has been tested with the iOS and macOS native clients. + +``` +# .env +USER=foo +PASSWORD=secret +``` + +``` +# docker-compose.yml +version: '3.7' + +services: + app: + image: cupcakearmy/radicale + env_file: .env + volumes: + - ./data:/data + ports: + - 80:5232 +``` + +The "migration" was done by exporting the calendars I had in Nextcloud with the native macOS calendar app and simply reimporting them into the new server, again with the native client on macOS. Same procedure with the contacts. Sync works like a charm and I'll never go back. + +### The downsides + +As with any project Seafile has some drawbacks compared to Nextcloud. Beside the obvious fact that Nextcloud has tons of plug-ins and Seafile does not, Seafile does store data and therefore files in blobs, so they are not visible to the host machine. That means that you cannot directly access lets say a `hello.txt` directly from the filesystem of the server you are hosting the service on. This might be a deal breaker for some people. + +Another thing that could bother some is that in the free version of Seafile there is no automatic garbage collection, so from time to time you should run the script to cleanup old data. + +## Conclusion + +To conclude the journey: It took me an evening to move everything, create the docker image for radicale and I could not be happier. Seafile feels so much more robust in comparison to the point that Nextcloud feels like a toy product. Of course this is not a fair comparison as Seafile only does file sync and not the other 10-20 big features Nextcloud brings to the table. However if you only use Nextcloud to sync files to your own cloud the comparison is not even close IMO. diff --git a/src/content/blog/matomo-vs-ublock-origin.md b/src/content/blog/matomo-vs-ublock-origin.md new file mode 100644 index 0000000..6e955d1 --- /dev/null +++ b/src/content/blog/matomo-vs-ublock-origin.md @@ -0,0 +1,185 @@ +--- +title: 'Matomo vs uBlock Origin' +date: '2021-01-28' +categories: + - 'general' +tags: + - 'blocker' + - 'matomo' + - 'stats' + - 'tracking' +coverImage: './images/luke-chesser-JKUTrJ4vK00-unsplash-scaled.jpg' +--- + +After [Ackee](https://github.com/electerious/Ackee) got an update and stopped working I wanted to search for an alternative to get some stats on my statically rendered site. As no server is used, I need some 3rd party service. + +I don't want to spy on people, nor set cookies and annoy people with consent banners if they only want to read a damn blog post. The goal is just get a feel for the traffic on the site. +This is important to mention as the next steps could sound a bit nefarious otherwise. +Data collected on this site is 100% anonymous and [GDPR](https://gdpr.eu/) compliant. + +
+ +![](images/luke-chesser-JKUTrJ4vK00-unsplash-1024x683.jpg) + +
+ +Photo by [Luke Chesser](https://unsplash.com/@lukechesser?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/data?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +Since Matomo is the de facto way to go, I spun up the Matomo server with my trusted docker-traefik setup and was up and running in no time. +( I'll share the config files if anyone is interested at the bottom. ) + +Then I quickly copied the JS tracker code in my main html template and thought that was it. **Wrong**. + +## The problem defaults. + +So turns out that Matomo, being widely used is of course included in many Ad-Blocker lists and therefore my stats did not work. Lets see why: +Basically all ad blockers work with lists. Those lists include pattern that if matched will be filtered out. Let's take a look at the default Matomo tracking code: + +``` + +``` + +We can see that my stats server is `stats.nicco.io`. And we also can see that the tracking script is loaded by `matomo.js`, which then sends the details to `matomo.php`. Well that is of course incredibly easy to block, and it is as you can see below: + +
+ +![](images/Screenshot-2021-01-28-at-12.12.59.png) + +
+ +Part of the EasyList Filter + +
+ +
+ +
+ +![](images/Screenshot-2021-01-28-at-12.14.03.png) + +
+ +Part of the EasyList Filter + +
+ +
+ +That won't work, and since most of the people that visit this site are probably developers which probably have some kind of ad blocker installed. + +## Solution time + +So after a short Ecosia search I landed on the blog of [Christian Mochow](https://christianmochow.de/author/christian-mochow/) that wrote a [blog post](https://christianmochow.de/beitraege/tools/catch-me-if-you-can-adblocker-umgehen-mit-matomo/) on this issue. I got the solution from his article. + +Luckily Apache has the famous Rewrite module, which will solve all our problems. I bet most of you already know where this is headed. + +We can create a `.htaccess` file in the root of our Matomo installation folder, to cloak our requests. + +``` +# .htaccess +RewriteEngine On +RewriteRule ^unicorn matomo.js +RewriteRule ^rainbow matomo.php +``` + +Now if we request `https://stats.nicco.io/unicorn` we actually get the response for `https://stats.nicco.io/matomo.js` and the same for `rainbow` and `matomo.php`. + +``` +// Replace in the client + +_paq.push(['setTrackerUrl', u+'matomo.php']); // Before +_paq.push(['setTrackerUrl', u+'rainbow']); // After + +g.src = u + 'matomo.js' // Before +g.src = u + 'unicorn' // After +``` + +**Awesome!** + +I had to create a minuscule `Dockerfile` as the `Rewrite` module is not enabled per default in the standard Matomo docker image. + +``` +# Dockerfile +FROM matomo +RUN a2enmod rewrite +``` + +## Responsible Usage + +Now as you can see it's incredibly easy to mask tracking stuff, and I bet there are a lot of people doing this in the wild. It is important to respect the privacy of your users and you should never store more data than you need and in the best case don't store data at all. + +**Anonymize as much as possible!** Matomo makes this easy. You can effortlessly delete 2 bytes of each ip address (half of the info), enforce strict no cookie tracking and automatically delete data after `x` days. Please do ❀️ + +### Config Files + +The `Dockerfile` and the `.htaccess` files are shown above. + +``` +# docker-compose.yml +version: "3.7" + +networks: + traefik: + external: true + +services: + db: + image: mariadb + command: --max-allowed-packet=64MB + restart: unless-stopped + volumes: + - ./data/db:/var/lib/mysql + env_file: .env + + app: + build: . + restart: unless-stopped + links: + - db + volumes: + - ./data/matomo:/var/www/html + - ./.htaccess:/var/www/html/.htaccess + env_file: .env + labels: + - traefik.enable=true + - traefik.docker.network=traefik + - traefik.port=80 + - traefik.backend=matomo + - "traefik.frontend.rule=Host:stats.nicco.io;" + networks: + - traefik + - default +``` + +``` +# .env +MYSQL_DATABASE=matomo +MYSQL_USER=matomo +MYSQL_PASSWORD= +MYSQL_RANDOM_ROOT_PASSWORD=yes + +MATOMO_DATABASE_HOST=db +MATOMO_DATABASE_ADAPTER=mysql + +MATOMO_DATABASE_DBNAME=matomo +MATOMO_DATABASE_USERNAME=matomo +MATOMO_DATABASE_PASSWORD= +``` + +See the [code for this website](https://github.com/cupcakearmy/nicco.io/blob/220643770385bebb05094b440c28441b49184556/src/template.html#L37-L64). diff --git a/src/content/blog/monitor-your-self-hosted-services-for-free.md b/src/content/blog/monitor-your-self-hosted-services-for-free.md new file mode 100644 index 0000000..5c8c22d --- /dev/null +++ b/src/content/blog/monitor-your-self-hosted-services-for-free.md @@ -0,0 +1,166 @@ +--- +title: "Monitor your self hosted services for free" +date: "2022-07-07" +--- + +Monitoring services requires external resources, as monitoring your server(s) from the server itself does not make sense. Renting a whole server for monitoring is a bit of a resources (and money) waste. + +
+ +![](images/daniele-franchi-g2fJ7d7eKSM-unsplash.jpg) + +
+ +Photo by [Daniele Franchi](https://unsplash.com/@daniele_franchi?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/radar?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +## Getting a free VM + +Luckily we can leverage the free tiers of many cloud providers. This gives us a free option and stability as they tend to be very reliable. Below is a list of free tiers available form the big 3 players. All of them offer a free VM per month, which is more than sufficient for our needs. + +- [Google Cloud Project](https://cloud.google.com/free/docs/gcp-free-tier/#compute) +- [Microsoft Azure](https://azure.microsoft.com/en-in/pricing/free-services/) +- [Amazon AWS](https://aws.amazon.com/free/?all-free-tier.sort-by=item.additionalFields.SortRank&all-free-tier.sort-order=asc&awsf.Free%20Tier%20Types=*all&awsf.Free%20Tier%20Categories=categories%23compute) + +Choose your preferred cloud, it does not really matter. I went with Google as I find the interface the nicest to use. For the OS of the VM I went which Ubuntu, but any Linux. + +## Setup + +For monitoring we will use [Uptime Kuma](https://github.com/louislam/uptime-kuma). It's an amazing free, open source monitoring tool, very similar to [UptimeRobot](https://uptimerobot.com/). For simplicity we will run it with Docker and Traefik. + +First we need to [instal docker](https://docs.docker.com/engine/install/debian/#install-using-the-repository) + +``` +curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg + +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \ + $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + +apt update +apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin +``` + +Also we want some basic firewall + +``` +apt install ufw +ufw allow 80 +ufw allow 443 +ufw allow 22 +ufw enable +``` + +Don't forget to point your DNS to the server. For example as a subdomain `status.example.org` + +## Depoly Uptime Kuma + +We only need a `docker-compose.yaml` file now and we should be up and running. I'll share the folder structure below. We could but everything in one compose file but I like to keep thinks tidy. + +``` +. +β”œβ”€β”€ kuma +β”‚ └── docker-compose.yaml +└── traefik + β”œβ”€β”€ docker-compose.yaml + └── traefik.yaml +``` + +### Traefik + +Lets start with Traefik. It will handle all our routing and TLS certificates. Remember to change the acme email down in the `traefik.yaml` + +``` +version: '3.8' + +networks: + default: + external: true + name: proxy + +services: + traefik: + image: traefik:2.8 + restart: unless-stopped + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./traefik.yaml:/etc/traefik/traefik.yaml:ro + - ./data:/data + labels: + - "traefik.enable=true" + + # HTTP to HTTPS redirection + - "traefik.http.routers.http_catchall.rule=HostRegexp(`{any:.+}`)" + - "traefik.http.routers.http_catchall.entrypoints=insecure" + - "traefik.http.routers.http_catchall.middlewares=https_redirect" + - "traefik.http.middlewares.https_redirect.redirectscheme.scheme=https" + - "traefik.http.middlewares.https_redirect.redirectscheme.permanent=true" +``` + +``` +#Define HTTP and HTTPS entrypoints +entryPoints: + insecure: + address: ":80" + secure: + address: ":443" + +#Dynamic configuration will come from docker labels +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + network: "proxy" + exposedByDefault: false + +#Enable acme with http file challenge +certificatesResolvers: + le: + acme: + email: me@example.org + storage: /data/acme.json + httpChallenge: + # used during the challenge + entryPoint: insecure +``` + +To get traefik running we just need to type the following + +``` +docker network create proxy +docker compose up -d +``` + +### Kuma + +The compose file for Kuma is compact. Don't forget to change the domain to yours. + +``` +version: '3.8' + +networks: + default: + external: true + name: proxy + +services: + kuma: + image: louislam/uptime-kuma:1 + restart: unless-stopped + volumes: + - ./data:/app/data + labels: + - traefik.enable=true + - traefik.http.routers.kuma.rule=Host(`status.example.org`) + - traefik.http.routers.kuma.entrypoints=secure + - traefik.http.routers.kuma.tls.certresolver=le +``` + +Now you can navigate to your new monitoring website and create and admin account and setup monitors, alert systems and so on. + +Many thanks to [Louis Lam](https://github.com/louislam) for creating and maintaining Utime Kuma! Consider donating! diff --git a/src/content/blog/react-code-splitting-made-simple-easily-reduce-bundle-js.md b/src/content/blog/react-code-splitting-made-simple-easily-reduce-bundle-js.md new file mode 100644 index 0000000..68aab9b --- /dev/null +++ b/src/content/blog/react-code-splitting-made-simple-easily-reduce-bundle-js.md @@ -0,0 +1,120 @@ +--- +title: 'React code splitting made simple. Easily reduce bundle.js' +date: '2019-07-21' +categories: + - 'coding' +tags: + - 'code-splitting' + - 'react' +coverImage: './images/jason-abdilla-jZWmw6007EY-unsplash-scaled.jpg' +--- + +On average right now **around 200-500kb of JS is sent** down the pipe to the client. Way to much for my personal taste. Since a lot of website use react, today we will look how to reduce that in a simple and easy way, applicable to almost any app. + +
+ +![](images/jason-abdilla-jZWmw6007EY-unsplash-scaled.jpg) + +
+ +Photo byΒ [Jason Abdilla](https://unsplash.com/@jabdilla_creative?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/search/photos/axe?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +Let's start with why our `bundle.js` even grows that much. In my opinion there are 2 causes for this: + +1. Devs mindlessly running `npm install`. +2. Loading **all** the JS, even if it is not needed in the current screen (e.g. the homepage). + +## Carefully select packages + +The first is not as easily solved if the app is already built. It requires your team to carefully choose your package and realise that often you don't need all the packages you are bundling. Often there is a lighter alternative. + +As an example: a lot of websites use [momentjs](https://github.com/moment/moment/): It's an awesome library, **but it weights [231.7kb](https://bundlephobia.com/result?p=moment)!** There is an even cooler alternative: [DayJS](https://github.com/iamkun/dayjs). It requires only [6.5kb](https://bundlephobia.com/result?p=dayjs) and shares the same API as moment, so there is no rewriting code. There are a lot of similar examples that can be made. If you have a big module, just search for a smaller alternative. + +#### TLDR; + +1. Use the [webpack-bundle-analyser](https://github.com/webpack-contrib/webpack-bundle-analyzer) for already existent packages. +2. Search for lighter alternatives to big packages. +3. **Before installing** check on [bundlephobia](https://bundlephobia.com/) how big your desired package is. + +## Code splitting & lazy loading + +Now to the real deal. The classic problem is that big websites don't split the JS that is sent to the client. This means that the website might receive the JS for the shopping section, while you are only waiting for the homepage to load. This is unnecessary and **waiting for a page to load is always a frustrating experience**. We can do better πŸ‘ + +How are we going to achieve this? **Code splitting & lazy loading.** +We are going to use 2 native react functions, so no external packages. + +- [lazy](https://reactjs.org/docs/code-splitting.html#reactlazy) +- [Suspense](https://reactjs.org/docs/code-splitting.html#suspense) + +Lazy & Suspense require React version **16.6** or newer. Also this does not work with server side rendering. + +**Lazy** is used to lazy load the component (...duh πŸ™„). This means that the code for the component is only downloaded when a component actually needs to be shown. + +**Suspense** is a handy wrapper for displaying a fallback while the component is loading. + +Let's see below how this is achieved: + +**From** + +``` +import MyList from './MyList' + +const App = () =>
+ // ... + + // ... +
+``` + +**To** + +``` +const MyList = lazy(() => import('./MyList')) + +const App = () =>
+ // ... + + + + // ... +
+``` + +This is in fact all you need to do. Now our component `MyList` will lazy load on necessity. Awesome! + +#### Bonus: Little helper function + +This can get repetitive though, so here is a little helper that basically wraps everything into one function: + +``` +export const Split = path => props => { + const Component = lazy(() => import(path)) + return Loading...}> + + +} +``` + +Now we can simply do the following: + +``` +import { Split } from './utils.jsx' + +const MyListLazy = Split('./MyList') + +const App = () =>
+ // ... + + // ... +
+``` + +There is a little codesanbox below with all the code if you wanna try for yourself (you should! πŸ˜‰) + + + +This concludes todays look at how to reduce the bundle size and use code splitting in react. Note that this does not work with server side rendering. diff --git a/src/content/blog/reduce-docker-compose-files-with-yaml-magic.md b/src/content/blog/reduce-docker-compose-files-with-yaml-magic.md new file mode 100644 index 0000000..d4b61ba --- /dev/null +++ b/src/content/blog/reduce-docker-compose-files-with-yaml-magic.md @@ -0,0 +1,53 @@ +--- +title: 'Reduce docker-compose files with YAML magic' +date: '2019-05-06' +categories: + - 'coding' +tags: + - 'docker' + - 'docker-compose' + - 'yaml' +coverImage: './images/guillaume-bolduc-259596-unsplash-scaled.jpg' +--- + +If you find yourself writing long docker-compose files because you need to specify the same things over and over again inside of the single services: **fear no more**! + +
+ +![](images/guillaume-bolduc-259596-unsplash-1024x741.jpg) + +
+ +Photo byΒ [Guillaume Bolduc](https://unsplash.com/photos/uBe2mknURG4?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/search/photos/container?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +_Without further ado:_ + +``` +version: '3.4' # min version 3.4 + +x-default: &default + restart: always + env_file: .env + + +services: + + web: + <<: *default + image: node + # blablabla + + db: + <<: *default + image: postgres +``` + +Thats it! Now both `web` and `db` inherit the properties of `x-default`. + +The `x-` prefix is a docker specific thing and is required. YAML support references also without the prefix. Also note that **version 3.4 or higher is required**. + +btw: I recently was looking into [Sentry](https://sentry.io/welcome/) and found this [docker-compose](https://github.com/getsentry/onpremise/blob/master/docker-compose.yml) file. Thats how I discovered it. diff --git a/src/content/blog/rust-in-python-made-easy.md b/src/content/blog/rust-in-python-made-easy.md new file mode 100644 index 0000000..a6d30d2 --- /dev/null +++ b/src/content/blog/rust-in-python-made-easy.md @@ -0,0 +1,223 @@ +--- +title: 'Rust in Python made easy' +date: '2020-01-01' +categories: + - 'coding' +tags: + - 'ffi' + - 'python' + - 'rust' +coverImage: './images/jonathan-chng-HgoKvtKpyHA-unsplash-scaled-1.jpg' +--- + +Python is truly amazing. With all that greatness generally there has to be a tradeoff and in the case of Python it's performance. + +Luckily there is an easy way to run computation intensive work in Rust, which is of course orders of magnitude faster. Let's see how! + +Overview + +1. [hello world example](#simple) +2. [parameters & returns](#params-returns) +3. [rust data types compared to pythons ctypes](#types) +4. [lists / arrays](#lists) +5. [complex data types handling](#complex) + +
+ +![](images/jonathan-chng-HgoKvtKpyHA-unsplash-scaled-1.jpg) + +
+ +Photo byΒ [Jonathan Chng](https://unsplash.com/@jon_chng?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/s/photos/run?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +Lets assume we want to run the following python code in rust. + +``` +def add(a, b): + return a + b + +add(1, 2) # 3 +``` + +Lets see the steps we need to take to achieve this: + +1. write some rust code +2. compile rust +3. import rust in python +4. run the rust code inside of python + +## Hello world example + +First lets create a new rust project by running: + +``` +cargo new rust_in_python +``` + +Then lets rename `src/main.rs` to `src/lib.rs` as we want a library and not standalone program. + +``` +mv src/main.rs src/lib.rs +``` + +Now we simply write a hello world function in rust + +``` +#[no_mangle] +fn hello() { + println!("Hello from rust πŸ‘‹"); +} +``` + +For every function that need to be available to other languages (in our case Python) through foreign function call (ffi) we will need to add the `#[no_mangle]` flag to it. + +The last step is to tell rust to compile to a dynamic library. To do so simply add the following to your `Cargo.toml` config file. + +``` +[lib] +crate-type = ["dylib"] +``` + +Now we are ready to build πŸš€ + +``` +cargo build --release +``` + +Now just create a `main.py` file and we can import and run our function. + +``` +from ctypes import CDLL + +lib = CDLL("target/release/librust_in_python.dylib") +lib.hello() +``` + +And if you run it you will be greeted from rust. No need to install, the `ctypes` package is included the standard python library. + +``` +python main.py +``` + +## With return & parameters + +Of course without giving parameters to the function and receiving its output this whole endeavour would be useless. + +Before we start I would like to remind you that python is untyped whereas rust of course is strongly typed. This means that we will need to tell python what types the parameters and the return of our rust function we have. + +First lets write the simple add function in rust + +``` +#[no_mangle] +fn add(a: f64, b: f64) -> f64 { + return a + b; +} +``` + +Don't forget to build again πŸ˜‰ + +``` +cargo build --release +``` + +Now to the python part + +``` +from ctypes import CDLL, c_double + +lib = CDLL("target/release/librust_in_python.dylib") + +lib.add.argtypes = (c_double, c_double) +lib.add.restype = c_double + +result = lib.add(1.5, 2.5) +print(result) # 4.0 +``` + +Lets see what is happening here: + +With `lib.add.argtypes` we must pass a tuple specifying how python should parse the data we pass to the `add` function. The `ctypes` package includes a list of different types we can use. [See the full list here](https://docs.python.org/3.8/library/ctypes.html#fundamental-data-types). + +The same happens with `lib.add.restype`. As you might have guessed this tells python what type is returned from the rust function. + +In our case it's all `c_double` as we are using `f64` in rust. + +## Data types in rust + +Lets see some other data types and their equivalent in rust. + +
PythonCRust
c_bool-
c_bytechari8
c_ubyteunsigned charu8
c_shortshorti16
c_ushortunsigned shortu16
c_intinti32
c_uintunsigned intu32
c_longlongi64
c_ulongunsigned longu64
c_floatfloatf32
c_doubledoublef64
+ +## Arrays & List + +So what about lists? Unfortunately I have not found a way to use Vectors for dynamic size arrays. So for now it's just fixed size arrays. + +###### Rust + +``` +#[no_mangle] +fn sum(arr: [i32; 5]) -> i32 { + let mut total: i32 = 0; + for number in arr.iter() { + total += number; + } + return total; +} +``` + +###### Python + +``` +from ctypes import CDLL, c_int + +lib = CDLL("target/release/librust_in_python.dylib") + +lst = [1, 2, 3, 4, 5] +# Create the memory of the list size +seq = c_int * len(lst) +arr = seq(*lst) + +result = lib.sum(arr) +print(result) +``` + +## Classes and complex data types + +Often it can be very useful to send and/or receive data in a structured, compact way. We can do this using structs. + +###### Rust + +``` +#[repr(C)] +pub struct Point { + pub x: f64, + pub y: f64, +} + +#[no_mangle] +fn greet_point(p: Point) { + println!("x: {}, y: {}", p.x, p.y); +} +``` + +###### Python + +``` +from ctypes import CDLL, Structure, c_double + +lib = CDLL("target/release/librust_in_python.dylib") + +class Point(Structure): + _fields_ = [ + ('x', c_double), + ('y', c_double) + ] + + +p = Point(x=1.2, y=3.4) +lib.greet_point(p) +``` diff --git a/src/content/blog/speed-up-your-docker-builds-with-dockerignore.md b/src/content/blog/speed-up-your-docker-builds-with-dockerignore.md new file mode 100644 index 0000000..656f428 --- /dev/null +++ b/src/content/blog/speed-up-your-docker-builds-with-dockerignore.md @@ -0,0 +1,74 @@ +--- +title: 'Speed up your docker builds with .dockerignore' +date: '2019-12-23' +categories: + - 'coding' +tags: + - 'docker' +coverImage: './images/thomas-kelley-t20pc32VbrU-unsplash-scaled-1.jpg' +--- + +So you ever wondered why your docker build takes so long to startup when all you do is adding a few files to your image and setting which command to run? + +Fear no more! `.dockerignore` to the rescue βš“οΈ. + +
+ +![](images/thomas-kelley-t20pc32VbrU-unsplash-scaled-1.jpg) + +
+ +Photo byΒ [Thomas Kelley](https://unsplash.com/@thkelley?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/s/photos/whale?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +Whenever you build a docker image the first thing you will always see is the following: + +```bash +docker build -t my-image . +Sending build context to Docker daemon 203.2MB +Step 1/6 : FROM ... +``` + +The important thing to note is the context with a size of `203.2MB` in this case. What does it mean? + +Every time a docker image gets built, it requires the context. By default it's simply the directory of your `Dockerfile`. Basically it's the folder from which you can add files to the image you are building. + +This means that for each build, the docker client sends the whole directory to the docker engine to build. +If you are unlucky this includes either the whole node_modules folder, maybe a virtual env folder for your python or simply everything in your dist folder. In my case it was a venv with 200+MB of data. + +This slows down the process significantly and if you are iterating on a dockerfile making only a few tweak this can be very tearing. + +> Waiting is the most boring (duh) and painful thing ever. +> +> Every person on this planet + +## Solution: `.dockerignore` + +Simply create a `.dockerignore` file inside of your context (basically in the same directory as your `Dockerfile`) and specify what to include, what to ignore. It works just like our trusted `.gitignore`. + +Im my case I only wanted to have my `requirements.txt` and the `.py` files inside of my `src` folder. + +###### .dockerignore + +``` +# Ignore everything +** + +!/requirements.txt +!/src + +**/__pycache__ +``` + +If I run `docker build` again watch what happens: + +```bash +docker build -t my-image . +Sending build context to Docker daemon 37.38kB +Step 1/6 : FROM ... +``` + +Awesome! The context shrunk from 200MB to under 50kB and the startup time of docker build was greatly reduced. This will help substantially if you have a lot of files, like e.g. a node_modules folder. diff --git a/src/content/blog/step-up-oauth-security-with-pkce.md b/src/content/blog/step-up-oauth-security-with-pkce.md new file mode 100644 index 0000000..633322b --- /dev/null +++ b/src/content/blog/step-up-oauth-security-with-pkce.md @@ -0,0 +1,79 @@ +--- +title: 'Step up OAuth security with PKCE' +date: '2019-07-10' +categories: + - 'general' +tags: + - 'ietf' + - 'oauth' + - 'security' +coverImage: './images/jielin-chen-pKQIpxzq0ZQ-unsplash-e1562783699383-scaled.jpg' +--- + +We all have used OAuth at some point. Whether with SSOs or a simple authorization server. While OAuth has multiple so called _flows_, the most commonly used are the _**code**_ and the _**implicit**_ flows. Today we will focus on the security aspect of the latter, pros/cons and solutions to make the _implicit_ flow (more) secure. + +I am going to assume some basic OAuth knowledge. [Here](https://medium.com/@darutk/the-simplest-guide-to-oauth-2-0-8c71bd9a15bb) is a basic explanation. [This](https://aaronparecki.com/oauth-2-simplified/) is a quick more in depth article. + +
+ +![](images/jielin-chen-pKQIpxzq0ZQ-unsplash-e1562783699383-1024x484.jpg) + +
+ +Photo byΒ [Jielin Chen](https://unsplash.com/@jerrychan0328?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/search/photos/fence?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +#### Quick refresher on implicit vs code flows + +As mentioned above there are [multiple flows](https://oauth.net/2/) in the [OAuth2 spec](https://tools.ietf.org/html/rfc6749), but in reality only the **_code_** and **_implicit_** are really used. The big difference between the two ist that in the **_code_** flow the there is a pre-shared password. This requires a backend, so the code flow cannot be used in SPAs. + +This is where traditionally the **_implicit_** flow comes into play. It is basically a played down version of his **_code_** brother. Instead of generating a code which is then used in combination with the password on the server side to get the token, in **the** _**implicit**_ **flow the token is returned directly**. + +## Issues with security in _implicit_ flows + +The implicit flow contains the `state` parameter. This is random value that the client sends to the authorization server. The server then redirects to the client including the `token` and `state` that was passed. This prevents CSRF and Replay attacks where an attacker could basically redirect some site to your callback path. The client should delete the state variable when getting the token. This is already pretty good, but we need to do more. You can read the details [in the spec](https://tools.ietf.org/html/rfc6749#section-10.12). + +**The server has no way of verifying that the original client actually got the token.** + +This is a big problem! Since the server cannot verify the identity of the original request it could end up giving the token to a 3rd party which did not make the request. Now an attacker has an access token. 😐 + +## PKCE to the saving πŸŽ‰ + +So how can our app demonstrate that it is the one that made the request? Of course the smart folks at [IETF](https://www.ietf.org/) have a solution. + +It's called **[Proof Key for Code Exchange](https://tools.ietf.org/html/rfc7636)**. In works in a similar way to the code flow, but instead of a password we use hashing. Confused? Let me explain: + +1. Your client wants to authenticate + 1. On the client side you generate a random string `verifier`. + 2. You then compute the SHA-256 hash of the `verifier` which gives you the `challenge` + 3. Your app makes the classic `/authorize?...&code_challenge=&code_challenge_method=S256` +2. The authorization server saves does the usual + 1. Redirect to user promps. + 2. User accepts. + 3. Server redirect to your callback path with a `code` +3. The client exchanges the `code` for a `token` + In the exchange request the client includes the original `verifier` which was used to generate the `challenge` hash. +4. Server verifies the request + The server takes the `verifier` and generates the `challenge` on its own, then checks whether it matches with the original one associated with the `code` +5. Server send code and client is authenticated. + +In this way, using `state` and the **PKCE** extension both the client and server are secured. The server can be sure that the token was sent only to the client from which the request originally was created and the client is safe agains CSRF & Replay attacks. **WIN WIN**. πŸ’ͺ + +Below is a diagram by [Auth0](https://auth0.com/) which helped me understand the whole process more easily. + +
+ +![](images/auth-sequence-auth-code-pkce-1024x833.png) + +
+ +[https://auth0.com/docs/flows/concepts/auth-code-pkce](https://auth0.com/docs/flows/concepts/auth-code-pkce) + +
+ +
+ +I hope you found this helpful. I was blown away by the simplicity of PKCE and how sometimes problems can be solved in very elegant ways thanks to smart people. diff --git a/src/content/blog/supporting-detecting-dark-mode-in-the-browser.md b/src/content/blog/supporting-detecting-dark-mode-in-the-browser.md new file mode 100644 index 0000000..5a3b49e --- /dev/null +++ b/src/content/blog/supporting-detecting-dark-mode-in-the-browser.md @@ -0,0 +1,149 @@ +--- +title: 'Supporting & detecting dark mode in the browser' +date: '2020-01-07' +categories: + - 'coding' +tags: + - 'css' + - 'dark-mode' + - 'media-query' +coverImage: './images/davisco-5E5N49RWtbA-unsplash-scaled-1.jpg' +--- + +How do you detect if a user of your website has a preference for light or dark theme? Or maybe has not chosen at all? + +We will look at a few ways how to detect and handle dark modes in 2020: + +1. [Pure CSS](#pure-css) +2. [JS](#js) +3. [React](#react) + +
+ +![](images/davisco-5E5N49RWtbA-unsplash-scaled-1.jpg) + +
+ +Photo byΒ [davisco](https://unsplash.com/@codytdavis?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/s/photos/contrast?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +## Pure CSS + +First lets have a look how we can do this using only CSS. There is a new css media query that is supported by [almost any browser](https://caniuse.com/#feat=prefers-color-scheme) right now. +**prefers-color-theme** + +###### Example Time + +```css +.box { + background: #eee; +} + +@media (prefers-color-scheme: dark) { + .box { + background: #000; + } +} + +@media (prefers-color-scheme: light) { + .box { + background: #fff; + } +} +``` + +Just like we use media queries for device width we can easily target specific CSS in case the user has a specific preference. + +For completeness, if the user has no preference you can use the media query `@media (prefers-color-scheme: no-preference)`. + +## JS + +#### Simple + +So how do we do the same thing in JS? There is a little helper called `window.matchMedia` ([also widely supported](https://caniuse.com/#feat=matchmedia)) wich takes a media query and tells us if the media query is true of false. + +###### Example + +```tsx +const isDark = window.matchMedia('(prefers-color-scheme: dark)') +isDark.matched // true or false +``` + +That simple. + +#### Reactive + +If we want to go reactive, the `matchMedia` function also allows us to set a listener, so every time the setting changes, we get notified and can act accordingly. + +###### Example + +```tsx +const isDark = window.matchMedia('(prefers-color-scheme: dark)') +isDark.addListener((event: MediaQueryListEvent) => { + event.matches // true or false +}) +``` + +## React + +And of course react. We love it, we use it. So I made [a little library](https://github.com/cupcakearmy/use-light-switch) because all the libs I found for react did not include typescript typing!! πŸ€• + +Of course it comes with hooks 🎣. + +```bash +yarn add use-light-switch +``` + +### `useLightSwitch()` + +This is the basic usage. + +```tsx +import { Mode, useLightSwitch } from 'use-light-switch' + + +const Simple: React.FC = () => { + const mode = useLightSwitch() + + if (mode === Mode.Dark) ... + + return ... +} +``` + +### `useModeSelector()` + +This is the more useful one IMO. + +```tsx +import React from 'react' +import ReactDOM from 'react-dom' +import { useModeSelector } from 'use-light-switch' + +const App: React.FC = () => { + const selected = useModeSelector({ + light: { color: 'green', name: 'Light' }, + dark: { color: 'red', name: 'Dark' }, + unset: { color: 'blue', name: 'Unset' }, + }) + + return ( +
+

Try switching your dark mode in macOS or Windows

+
+ {selected.name} +
+
+ ) +} + +ReactDOM.render(, window.document.getElementById('root')) +``` diff --git a/src/content/blog/tales-of-learning-go-from-ts.md b/src/content/blog/tales-of-learning-go-from-ts.md new file mode 100644 index 0000000..78193fb --- /dev/null +++ b/src/content/blog/tales-of-learning-go-from-ts.md @@ -0,0 +1,144 @@ +--- +title: 'Tales of learning Go (from TS)' +date: '2021-04-17' +categories: + - 'coding' +tags: + - 'autorestic' + - 'go' + - 'restic' +coverImage: './images/mark-autumns-Ssr26I0QWVY-unsplash-scaled.jpg' +--- + +The story starts about a week ago (April 2021) when I finally got around rewriting a tool called [autorestic](https://github.com/cupcakearmy/autorestic). It's a CLI wrapper around restic, the amazing backup utility. +This is not a guide or tutorial, just a diary of experiences of the process of coding Go for the first time. I'm far far far away from understanding Go at it's fullest, so veterans please correct me! + +### Background + +I started `autorestic` in Typescript as at the time it was the main language I was using and I simply love it. The issue was that JS requires a runtime and does not compile, which is not ideal for a standalone CLI. That ment using [_pkg_](https://github.com/vercel/pkg/) and while originally _pkg_ was quite an awesome project, the binaries are large (`70Mb+`) and the support is starting to dwindle with critical issues not being addressed. This meant that I couldn't create ARM builds due to a pending issue that had been open for more than a year by now. + +The languages that came to mind were Rust, Go, Kotlin and Swift and I wanted to get to know a new language in the process. The choice came down to Go as it seemed to be easy to learn and is used extensively where I work at, so that was an added bonus. + +What follows are the experiences I made as a mostly TS/JS/Python developer going into Go and learning as I went along. I did not have prior Go experience except some hello world examples here and there. + +
+ +![](images/mark-autumns-Ssr26I0QWVY-unsplash-1024x683.jpg) + +
+ +Bridge + +
+ +
+ +## Setup + +I code on macOS so setup was easy: just run `brew install go` and be done with it. Or so I thought so. The whole `GOPATH` and `GOROOT` is not as straight forward. To this day I have not completely comprehended what they do so I will not even try explain it. I set mine to the following and everything worked after that. + +```bash +export GOPATH="$HOME/.go" +export GOROOT=(brew --prefix go)"/libexec" +export PATH="$PATH:$GOPATH/bin:$GOROOT/bin" +``` + +VSCode on the other hand actually just works without an issue, which was pleasant. As I will discover later the whole tooling is a real pleasure to work with. + +### Structure + +There are almost no fixed rules for project structure, but many conventions within the community. A very good starting guide is taking a look at this repo: [golang-standards/project-layout](https://github.com/golang-standards/project-layout). There every "conventional" folder has an explanation of it's meaning. + +A (kind of) weird standard in the Go world is not having a `src` folder, but rather everything is at root level of a project. + +## Basics + +Now to the actual coding. There is come getting used to so buckle up and join the ride. + +#### Types + +Being a typed language Go has a set of basic types and types built on top of them. Basic types are the classic `string`, `int`, `bool`, etc. Then we have arrays (fixed size) and slices (dynamic). We also get `map` and `struct`. + +Complex types like maps and structs can be `nil` (null), while basic types have default values and cannot be `nil`. strings default to `""` and ints to `0`. This means that the only way in Go to check if a value has been initialised (e.g. in a struct) is if the value is different from the default one. + +A gotcha for me was that in Go almost everything is copied by value and not reference. Therefore also the `range` oparator which is used to loop arrays, slices and maps will copy by value and not reference, which for JS and Pyton people will be confusing as there complex objects are always copied by reference. + +#### Missing language features + +Before you start searching "how do I do X in Go?" I will save you the time and give you a list of features that are **not** available in go. + +- No ternary operator +- No optional/default function parameters +- No function overloading +- No generics +- No sum/union type + +So don't bother searching for them, they are not there. Which at first seems quite limiting, and after a week still feel the same. If you ask the Go community the overwhelming answer is that it's by choice to keep the language "lean" and don't bloat it with unnecessary syntax. I'll get back to this point later. + +#### Imports + +In Go you try to group code by functionality, which is quite common in programming languages. These are called "packages" and the import structure is similar to Java. Nothing out of the ordinary. A little gotcha: Go cannot resolve cyclic imports. All in all they work quite well. + +External dependencies have a native solution called _go modules_ and work as you would expect it. There is a descriptory file where all your dependencies are listed called `go.mod` and a lock file `go.sum`, no concerns here. + +#### Error handling + +One of the most obvious patterns in Go is the way errors are handled. Basically whenever a function can "throw" an error instead of _throwing_ and _catching_ in Go we return a nullable (`nil`) value and then check that. + +```go +package main + +import ( + "errors" + "fmt" +) + +func main() { + if result, err := divide(4, 2); err != nil { + fmt.Println("We have broken math") + } else { + fmt.Println("The result: ", result) + } +} + +func divide(x, y int) (int, error) { + if y == 0 { + return 0, errors.New("Cannot divide by 0") + } + return x / y, nil +} +``` + +This is definitely different to what I was used to, but I enforces error checking and I think it's actually not a stupid design concept. The only negative consequence of that is that you will write the following statement over and over and over and over again. Get's quite repetitive. + +```go +if err != nil { + return err +} +``` + +## Speed + +What go lacks in features it makes up in speed. There is no questioning the ease of use and the effective time to code something can be quite low. There will be repetitive tasks, but go makes it possible to move at incredible speed. There is very little holding you back. + +The compiler is super fast, which was one of the design goals of the language. Arguably that is not a difficult task to achieve though as the language has virtually no features a compiler needs to actually think about. + +## Tooling + +Tooling is one of the areas where Go really excels and I have yet to find a comparable language. Many things are opinionated, which I love. There is the default formatter, linter, compiler, etc. Everything simply works and has never stood in the way of coding and getting things done. It was a very refreshing experience, especially after coming from a Electron project where the whole bundling and rest was a real pain to deal with. + +#### Cross compilation + +Compiling for multiple targets is frankly to easy to be true. You simply pass two environment variables to the compiler and he does the rest. No matter if solaris, arm, mips, windows. It does it all just fine. + +```bash +GOOS=linux GOARCH=arm go build main.go +GOOS=darwin GOARCH=amd64 go build main.go +# ... +``` + +## What does it feel like to write GO? + +I have quite mixed feelings about Go. On one hand the tooling and speed really are compelling advantages. However the language omits too many features in favor of remaining as _lean_ as possible. Having to iterate an array manually just to check if it contains a given element feels really outdated and does not help readability. Also in go using constants is basically unheard of, making code harder to understand and review. Not having optional/default parameters for functions often leads to code duplication as it is way easier to copy paste than to write a "one fits all" function. This is mostly due to the unavailability of generics which make it impossible to write a general `includes(element)` function for arrays of slices for example. + +Then again I completely rewrote my CLI tool from scratch in about a week, including cross compiling for multiple platforms while never having touched Go before. That is quite something and not many compiled langauges could have done this. And after getting used to the (many) limitations of the language it's fun to code Go. diff --git a/src/content/blog/telegram-bots-are-easy.md b/src/content/blog/telegram-bots-are-easy.md new file mode 100644 index 0000000..9e18157 --- /dev/null +++ b/src/content/blog/telegram-bots-are-easy.md @@ -0,0 +1,319 @@ +--- +title: 'Telegram bots are easy' +date: '2019-06-24' +categories: + - 'coding' +tags: + - 'automatization' + - 'bot' + - 'python' + - 'telegram' +coverImage: './images/arseny-togulev-1513013-unsplash-scaled.jpg' +--- + +Recently I've been frustrated with mobile stock apps so I started thinking about solutions. a whole app would have been an unnecessary overkill, after all I just want to get market data about my stocks with some indicators to go along. A perfect use case for a telegram bot! + +Today we will have a look at how telegram bots are written and show some base concepts around the platform. We are going to leverage [python-telegram-bot](https://python-telegram-bot.org/) ([latest docs](https://python-telegram-bot.readthedocs.io/en/latest/)). + +#### What we will build today: + +1. A very simple bot that just replies a picture of a doggo whenever we send `/woof`. +2. A bot that has **persistence** and will save a list of items we can add. +3. (bonus round) Dockerize the bot for running it everywhere. + +
+ +![](images/arseny-togulev-1513013-unsplash-1024x576.jpg) + +
+ +Photo byΒ [Arseny Togulev](https://unsplash.com/@tetrakiss?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/search/photos/robot?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +#### Here we go πŸ€– + +The first thing we need is a bot. To register one you need to talk to the [@BotFather](https://telegram.me/BotFather) (with your normal telegram account). Then simply enter the `[/newbot](tg://bot_command?command=newbot)` command. You will be asked the name and the @handle for your bot. The handle need to end in `bot`. +After that he will answer with your access token. + +
+ +![](images/register-bot.jpg) + +
+ +Response after successful bot registration + +
+ +
+ +## How do bots work? + +
+ +![](images/IMG_0160-1024x436.jpeg) + +
+ +very badly drawn representation of the bot + +
+ +
+ +In its most simple form bots are simple programs that listen to _commands_ send by a user. The cannot initiate a conversation, a user must always message the bot first. After initial contact, the bot can send as many messages as he wants. + +Your code connects to the telegram servers and gets notified if new messages/commands have been sent. Then based on the user input your bot can reply with a message, audio, photo or use external services like sending a slack notification, whatever, there are no real limits. + +When the user firsts contacts a bot telegram automatically sends a `/start` command which you can use to initialise the bot. When a bot is deleted by a user the corresponding `/stop` command is issued. + +## Doggo Bot + +We are going to create a virtual environment for the project and install the dependencies. Our starting point will be the basic bot skeleton from which we will fill out the functionality. + +Note: we need version 12 of _python-telegram-bot_, as the API changed quite a bit in the previous releases. + +```bash +python3 -m venv env +source env/bin/activate +pip install python-telegram-bot==12.0.0b1 +``` + +```python +from telegram.ext import Updater, CommandHandler + +TOKEN = 'myTelegramBotToken' + +def main(): + updater = Updater(TOKEN, use_context=True) + dp = updater.dispatcher + + updater.start_polling() + updater.idle() + + +if __name__ == '__main__': + main() +``` + +Great, now lets write a function to respond to a simple `/ping` message. + +``` +// ... + +def pong(update, context): + update.message.reply_text('Pong') + + +def main(): + updater = Updater(TOKEN, use_context=True) + dp = updater.dispatcher + + dp.add_handler(CommandHandler('ping', pong)) + + updater.start_polling() + updater.idle() + +// ... +``` + +When we now send `/ping` to the bot we get a _Pong_ back. Neat! + +
+ +![](images/IMG_1709.jpeg) + +
+ +result of pong message + +
+ +
+ +#### But we want doggos! 🐢 + +```python +def get_image_url(): + allowed_extension = ['jpg', 'jpeg', 'png'] + while True: + url = requests.get('https://random.dog/woof.json').json()['url'] + file_extension = re.search("([^.]*)$", url).group(1).lower() + if file_extension in allowed_extension: + break + return url + + +def woof(update, context): + update.message.reply_photo(photo=get_image_url()) + + +def main(): + //... + + dp.add_handler(CommandHandler('ping', pong)) + dp.add_handler(CommandHandler('woof', woof)) + + //... +``` + +We define a second command that listens to the `woof` word. Then we have a little helper function that retrieves a url. That url is then passed to the `reply_photo` method and we are done! + +We could also pass a [bite-like](https://docs.python.org/3/glossary.html#term-bytes-like-object) object to the `photo` parameter instead of a string containing the url if we would have an actual photo. + +
+ +![](images/IMG_1710.jpeg) + +
+ +reply to woof + +
+ +
+ +## Persistence Bot + +The doggo bot is a good hello world example, but without persistence a lot of bots would not work. If you want to save something you will need to have some kind of **user base storage**. + +For this we will implement a little bot that saved a list of items that a user can add/delete. A bit like a shopping list if you want. + +#### Commands + +- Add `/add avocados` +- Delete `/delete avocados` +- List all `/all` +- Delete all `/clear` + +First the basic setup again. We will import `PicklePersistence` which uses the python _pickle_ object to save the data on your drive. We need to initialise it and pass it to the `Updater` class. + +```python +from telegram.ext import Updater, CommandHandler, CallbackContext, PicklePersistence +from telegram import Update + +TOKEN = 'myTelegramBotToken' + +persistence = PicklePersistence('./db') + + +def main(): + print('Started πŸš€') + updater = Updater(TOKEN, use_context=True, persistence=persistence) + dp = updater.dispatcher + + updater.start_polling() + updater.idle() + + +if __name__ == '__main__': + main() +``` + +Before we beginn lets define 2 helper functions: + +```python +def get_list(context: CallbackContext) -> dict: + return context.user_data.setdefault('list', {}) +``` + +`get_list` will return the object that is unique to every user. The python library generously offers a `user_data` object inside of the `context` parameter that gets passed to each handler. We use `setdefault` in order to get the already present dict of the user or set it to an empty one if undefined. + +```python +def parse_command(update: Update) -> (str, str): + key, value = update.message.text.split(' ', 1) + return key, value +``` + +`parse_command` simply helps us interpret the message of the user. If a user sends us `/add avocado` we only want the _avocado_ part. We simply split at the first space. + +Lets finally add all the methods. + +```python +//... + +def get_list(context: CallbackContext) -> list: + return context.user_data.setdefault('list', []) + + +def parse_command(update: Update) -> (str, str): + key, value = update.message.text.split(' ', 1) + return key, value + + +def list_add(update: Update, context: CallbackContext): + key, value = parse_command(update) + get_list(context).append(value) + update.message.reply_text('Saved πŸ’Ύ') + + +def list_delete(update: Update, context: CallbackContext): + key, value = parse_command(update) + get_list(context).remove(value) + update.message.reply_text('Deleted πŸ—‘') + + +def list_all(update: Update, context: CallbackContext): + items = get_list(context) + update.message.reply_text('\n'.join(items) if len(items) > 0 else 'List empty 😒') + + +def list_clear(update: Update, context: CallbackContext): + get_list(context).clear() + update.message.reply_text('Cleared 🧼') + + +def main(): + //... + + dp.add_handler(CommandHandler('add', list_add)) + dp.add_handler(CommandHandler('delete', list_delete)) + dp.add_handler(CommandHandler('all', list_all)) + dp.add_handler(CommandHandler('clear', list_clear)) + + //... +``` + +![](images/IMG_1711-653x1024.jpeg) + +I believe telegram bots can be a great alternative if a full fledged app is an overkill. There are a lot of featured that where not mentioned here, especially custom keyboards. + +## Dockerize it + +``` +# requirements.txt + +python-telegram-bot==12.0.0b1 +``` + +```Dockerfile +# Dockerfile + +FROM python:3.7-slim +WORKDIR /app + +COPY requirements.txt . +RUN pip install -r requirements.txt && rm -rf /root/.cache + +COPY src . +CMD ["python", "-u", "/app/Mercatus.py"] +``` + +```yaml +# docker-compose.yml + +version: '3.6' + +services: + bot: + build: . + restart: unless-stopped + command: ['python', '-u', '/app/Mercatus.py'] + volumes: + - ./data.db:/app/data.db +``` + +Thats it! I hope you enjoyed the guide. πŸ‘‹ diff --git a/src/content/blog/the-essential-no-excuses-security-checklist-for-modern-websites.md b/src/content/blog/the-essential-no-excuses-security-checklist-for-modern-websites.md new file mode 100644 index 0000000..fa9a3ea --- /dev/null +++ b/src/content/blog/the-essential-no-excuses-security-checklist-for-modern-websites.md @@ -0,0 +1,226 @@ +--- +title: 'The essential no-excuses security checklist for modern websites.' +date: '2019-09-16' +categories: + - 'security' +tags: + - 'headers' + - 'security' +coverImage: './images/milkovi-kYlYwQze5vI-unsplash-1-scaled.jpg' +--- + +The web and its security has come a long way. As always in security there are constantly ways to improve your defences agains bad actors. This is a list of quick and easy, yet powerful tools that all devs should be using. + +**Update** _18 Sep 2019 @ 16:02_ +As the reddit user [zfa](https://www.reddit.com/user/zfa/) suggested I included the link to the Mozilla Observatory for automatic auditing. + +1. Checklist + 1. [HTTPS](#https) + 2. [TLS Versions](#tls) + 3. [Ciphers](#ciphers) + 4. [HSTS](#hsts) + 5. [CSP](#csp) + 6. [X-Frame-Options](#frame) + 7. [X-Content-Type-Options](#content-type) +2. [Useful Libraries & Tools](#libraries) +3. [Considerations](#considerations) + 1. [Cookies or LocalStorage for JWTs?](#jwt) + 2. [Advanced HTTPS practices](#advanced) +4. [Sources](#sources) + +
+ +![](images/milkovi-kYlYwQze5vI-unsplash-1-scaled.jpg) + +
+ +Photo by [MILKOVÍ](https://unsplash.com/@milkovi?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +## 1\. HTTPS + +Let's start with the most obvious. It's almost 2020 and websites not using HTTPS are simply being irresponsible. There is no reason to run plaintext http in the era of [Letsencrypt](https://letsencrypt.org/) where getting a certificate is **easy and free**. I won't go over how to configure that, there are tons of resources out there and generally its as simple as adding a line in your config file. +Also redirect all the http traffic to https automatically, basic stuff. + +## 2\. TLS Versions + +[97.65% of global users](https://caniuse.com/#feat=tls1-1) support TLS version 1.2, so go disable anything below that in your server as it has knows vulnerabilities! + +**Configuration** + +``` +# Nginx +ssl_protocols TLSv1.2; +``` + +``` +# Apache +SSLProtocol -all +TLSv1.2 +``` + +``` +# Traefik +[entryPoints] + [entryPoints.https] + [entryPoints.https.tls] + minVersion = "VersionTLS12" +``` + +## 3\. Ciphers + +Similar to the TLS version you should avoid using anything different than AES or ChaCha20. Limit the ciphers to something secure and modern. + +**Configuration** + +``` +# Nginx +ssl_prefer_server_ciphers on; +ssl_ciphers +'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256'; +``` + +``` +# Apache +SSLCipherSuite +ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256 +SSLHonorCipherOrder on +SSLCompression off +SSLSessionTickets off +``` + +``` +# Traefik +[entryPoints] + [entryPoints.https] + [entryPoints.https.tls] + cipherSuites = [ + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", + "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", + ] +``` + +## 4\. HSTS + +Once your website runs on HTTPS it's a good idea to tell the browser not to use HTTP anymore. Thats what the HTTP Strict Transport Security (HSTS) is designed for. + +``` +Strict-Transport-Security: max-age=31536000; includeSubDomains +``` + +This basically tells the browser to only talk to your domain via HTTPS for the next year. You can exclude the `includeSubDomains` if you want to just target your root. + +**Configuration** + +``` +# Nginx +add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; +``` + +``` +# Apache +Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains" +``` + +## 5\. Content Security Policy (CSP) + +This awesome HTTP header is incredibly powerful! It allows you to specify the allowed origins for all kind of files that will be loaded into your website. + +The basic idea is: "_Javascript can only be loaded from my domain, images only from the unsplash domain and fonts only are only allowed from google fonts"._ + +Awesome right? The header for that example would look like this: + +``` +Content-Security-Policy: "default-src 'self'; img-src https://unsplash.com/; font-src https://fonts.googleapis.com" +``` + +This allows a dev to specify exactly what resources are allowed to load from what domains. Don't worry, you don't need to remember the exact syntax**:** +**Use the generator**: [https://www.cspisawesome.com/](https://www.cspisawesome.com/) + +## 6\. X-Frame-Options + +This is a basic one, but it should not be forgotten. It prevents your website to be displayed inside another one. This prevents shit tons of possible attack vectors. Simply set the header and you're done. + +``` +x-frame-options: SAMEORIGIN +``` + +**Configuration** + +``` +# Nginx +add_header x-frame-options "SAMEORIGIN" always; +``` + +``` +# Apache +header always set x-frame-options "SAMEORIGIN" +``` + +## 7\. X-Content-Type-Options + +Again, a little header that prevents lots of damage. By setting the Content Type header you prevent the browser from guessing what file contents a downloaded resource is. Basically if `/image.jpeg` is actually a `.js` file the browser would still run it if you don't set the header + +``` +x-content-type-options: nosniff +``` + +**Configuration** + +``` +# Nginx +add_header x-content-type-options "nosniff" always; +``` + +``` +# Apache +header always set x-content-type-options "nosniff" +``` + +## Libraries & Tools + +#### Tools + +For some automatic auditing of your website you can use the excellent [Mozilla Observatory](https://observatory.mozilla.org/) tool. You can scan a domain and receive information on Header & TLS. + +#### Libraries + +For a lot of this headers there are some good libraries for automating this which already have good default values, so most of them are plug and play. + +
Express (Node)helmet
ASP.NETNWebsec
Django (Python)django-csp
Dropwizard (Java)dropwizard-web-security
Flask (Python)Talisman
Gosecuresecureheader
Hapi (Node)blankie
Koa (Node)koa-helmet
PHPSecure Headers
Ruby (and Rails)Secure Headersrack-secure_headers
+ +## Considerations + +I did not talk about the XSS header, since it's not supported at all in Firefox and Chromium will remove it in the near future, so I think it gives a false sense of security to devs not being vulnerable to XSS if they set the header. + +Also I did not touch on the new Feature Policy header wich is currently being drafted. It's very cool and will help a lot in the future. It allows websites to specify what features should be allowed to run, so e.g. if my site does not need the accelerometer just turn it off. That way no 3rd party code is able to access it. Very nice addition, but at the time of writing it's still very alpha and basically not supported. + +### Where to store JWTs? + +Most websites nowadays make use of JWTs for the authentication. A common question people have is: Where do I store my token? Cookies or LocalStorage? **TLDR: LocalStorage.** + +The general knowledge is that LocalStorage is not vulnerable to CRFS and Cookies not to XSS. However as the reddit user [Devstackr](https://www.reddit.com/user/Devstackr/) described [here](https://www.reddit.com/r/reactjs/comments/cubfsa/local_storage_vs_cookies_authentication_tokens/) if you have a XSS vulnerability also your authentication via cookie is compromised, as the injected code can do requests on behalf of the authenticated user. + +So while your token cannot be directly stolen from the victim, the attacker can still do everything, including changing the password for example. Additionally you don't need to worry about CRFS at all, which is a huge bonus. + +### Advanced Practices + +Large websites should additionally consider the following security features: + +- Certificate Authority Authorization (CAA) DNS record which specifies what CA is allowed to sign certificates for the served domain. +- HTTP Public Key PinningΒ (HPKP) provides the option to validate the certificate via DNS record. If misconfigured this can break you entire site, so use carefully! + +## Sources / Further reading + +- [https://www.keycdn.com/blog/http-security-headers](https://www.keycdn.com/blog/http-security-headers) +- [https://www.youtube.com/watch?v=-DNNlBYIyxQ](https://www.youtube.com/watch?v=-DNNlBYIyxQ) +- [https://helmetjs.github.io/](https://helmetjs.github.io/) +- [https://www.acunetix.com/blog/articles/tls-ssl-cipher-hardening/](https://www.acunetix.com/blog/articles/tls-ssl-cipher-hardening/) +- [https://www.cspisawesome.com/](https://www.cspisawesome.com/) +- [https://observatory.mozilla.org/](https://observatory.mozilla.org/) diff --git a/src/content/blog/the-powerful-es6-proxy-object.md b/src/content/blog/the-powerful-es6-proxy-object.md new file mode 100644 index 0000000..f1fdc20 --- /dev/null +++ b/src/content/blog/the-powerful-es6-proxy-object.md @@ -0,0 +1,117 @@ +--- +title: 'The powerful ES6 proxy object' +date: '2019-05-31' +categories: + - 'coding' +tags: + - 'es6' + - 'javascript' + - 'proxy' +coverImage: './images/alina-grubnyak-1254785-unsplash-scaled.jpg' +--- + +Today: yet another ES6 feature that I think most people don't know about. + +> "TheΒ **Proxy**Β object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc)." +> +> [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) + +
+ +![](images/alina-grubnyak-1254785-unsplash-1024x683.jpg) + +
+ +Photo byΒ [Alina Grubnyak](https://unsplash.com/@alinnnaaaa?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +Let's dive right in! πŸ„β€β™‚οΈ + +#### Basic example: + +```tsx +const handler = { + get: (obj, key) => (key in obj ? obj[key] : 42), +} + +const proxy = new Proxy({ a: 1 }, handler) + +console.log(proxy.a, proxy.b) // 1 42 +``` + +We start with a plain object `{a: 1}` and we assign it a handler. Our handler intercepts the `get` statement of a normal object and we can define our own custom logic for it. In this case we return the value if it exists, otherwise a 42. + +We can se that calling `proxy.b` will output the 42, as it is not set in the root object. + +## The handler + +The second argument of the Proxy function is what is called the **handler**. This is simply an object with functions that define the logic of the operations, which are called **traps** (as they get triggered). The available traps are as follows: + +- get +- set +- has +- deleteProperty +- ownKeys +- apply +- construct + +There are a few more, but these are the basic ones. Here is the [full list](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#Methods_of_the_handler_object). + +#### Some practical examples + +```tsx +var handler = { + set: (obj, key, value) => { + if (key === 'wheels') + if (!Number.isInteger(value) || value % 2 !== 0) throw new Error('Wheels need to come in pairs') + obj[key] = value + }, +} + +// πŸš— +var car = new Proxy({}, handler) + +car.wheels = 4 +car.wheels = 3 // Throws an error +``` + +Here we can see that we can define constrains for the setter. In this case we say that the key `wheels` needs to be and integer and a multiple of 2. This is a trivial example, but we can imagine how useful this can be with forms. + +```tsx +const validators = { + username: /^[A-z]{3,}$/, + password: /(?=.*[\d]).{8,}/, +} + +const handler = { + set: (obj, key, value) => { + if (key in validators && validators[key].test(value)) obj[key] = value + else throw new Error('Invalid input') + }, +} + +const form = new Proxy({}, handler) + +form.username = 'abc' +form.username = 'this contains spaces' // -> Error + +form.password = 'atleast1digit' +form.password = 'withoutanydigitthiswillthrowanerror' // -> Error + +form.something = true // -> Error, since it does not exist in the validator object +``` + +In this example we can validate an object from a form for example. We have a validator object that contains some regex. The Proxy then only allows keys that are inside of the validator and only if they pass the corresponding regex. Vary handy πŸ‘Œ + +## In real life + +This only available in ES6 (ES2015) but ~92% of the browsers [support it](https://caniuse.com/#feat=proxy). Some real life applications that make use of this are [MobX](https://github.com/mobxjs/mobx) (since v5.0). + +In MobX this allows the developer to skip the tedious `mapStateToProps` and `mapDispatchToProps` and just let MobX handle it. Since with the help of proxies MobX can know what parts of the state are being used by listening on the `get` trap of the state and can then only render the components when the props they are using in the `render()` function are being used. + +There a small package that does the same for the redux world: [https://github.com/dai-shi/reactive-react-redux](https://github.com/dai-shi/reactive-react-redux) + +I hope this helped and was interesting! πŸ‘‹ diff --git a/src/content/blog/use-traefik-and-regexp-to-bypass-adblockers.md b/src/content/blog/use-traefik-and-regexp-to-bypass-adblockers.md new file mode 100644 index 0000000..4afb9ad --- /dev/null +++ b/src/content/blog/use-traefik-and-regexp-to-bypass-adblockers.md @@ -0,0 +1,87 @@ +--- +title: 'Use Traefik and RegExp to bypass AdBlockers' +date: '2022-01-10' +categories: + - 'general' + - 'security' +tags: + - 'ad-blocker' + - 'tracking' + - 'traefik' + - 'umami' +--- + +**This will be a controversial one, so let me explain the motivations first.** + +I was using Matomo for collecting traffic metrics on different websites, however it's kind of bloated for what I need to do. My goal is to get a grasp on viewership, **without collecting personal data** or any fancy analytics. I wanted to try out Umami as it seems simple and is privacy respecting first (including GDPR), without the need to tweak settings. + +The problem was that my AdBlocker was not letting request through and since most of the people visiting this site will probably devs they surely have some AdBlocker installed. + +**FYI**: All the data collected on this site are accessible and visible to anyone [here](https://spectare.nicco.io/share/Xklie3UU/Nicco). + +
+ +![](images/hyeryi-sVk8nrCQ06g-unsplash-1024x683.jpg) + +
+ +Photo by [ι€Έ 韩](https://unsplash.com/@hyeryi?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/intersection?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +## The issue + +Lets have a look at the typical Umami code: + +```html + +``` + +The problem is that most block list have `umami.js` on it, which make measuring anonymous traffic impossible. Now blocklists are very static (for performance reasons) and therefore quite easy to circumnavigate. + +## Rerouting + +What do you do when the street you are driving on is blocker? You take the detour. + +The solution is not to use `umami.js` directly, but redirect it form another name. With traefik this is very easy using the [ReplacePathRegex middleware](https://doc.traefik.io/traefik/v2.0/middlewares/replacepathregex/). Let's see how: + +```yaml +version: '3.8' + +networks: + proxy: + external: true + +services: + umami: + image: ghcr.io/mikecao/umami:postgresql-latest + networks: + - default + - proxy + labels: + - traefik.enable=true + - traefik.http.routers.umami.rule=Host(`spectare.nicco.io`) + - traefik.http.routers.umami.entrypoints=secure + - traefik.http.routers.umami.tls.certresolver=le + - traefik.http.routers.umami.middlewares=umami-rewrite + - traefik.http.middlewares.umami-rewrite.replacepathregex.regex=/unicorn.js + - traefik.http.middlewares.umami-rewrite.replacepathregex.replacement=/umami.js + + db: + image: postgres:12-alpine + ... +``` + +We need to configure the `regex` and `replacement` parameters for the `replacepathregex` middleware. In my case I chose to use `/unicorn.js` and redirect it internally to `/umami.js`. This way the internal service is doing business as usual while ad blockers do not block the request. + +## Responsibility + +As you can imagine this would enable far more nefarious use cases. You could also mask it as something like `23hf872.min.js` or whatever. So use with caution and **always keep in mind your users privacy and data!** diff --git a/src/content/blog/why-i-love-js-but-sometimes-i-feel-we-shoot-ourself-in-the-foot.md b/src/content/blog/why-i-love-js-but-sometimes-i-feel-we-shoot-ourself-in-the-foot.md new file mode 100644 index 0000000..092d154 --- /dev/null +++ b/src/content/blog/why-i-love-js-but-sometimes-i-feel-we-shoot-ourself-in-the-foot.md @@ -0,0 +1,36 @@ +--- +title: "Why I love JS but sometimes I feel we shoot ourself in the foot." +date: "2020-05-29" +categories: + - "general" +--- + +Let's start by saying this: I absolutely love JS & Typescript, they are my favourite languages and I would not want to live without them. + +Now that being said, whenever I look at Go or Python for example the quality of packages cannot be compared. This is I feel the biggest weakness of the JS ecosystem and is frustrating because it could be so much better. + +## Quality vs Quantity + +Basically this is my main problem with the ecosystem. Whenever we look at other languages generally speaking there is one, maybe two packages for each use case they are trying to solve. And there is a consensus inside of that coding community what the preferred tools should be. + +In the NPM world there are always what feels like never ending alternatives that try to solve the same problems over and over again. + +So an economist might think: "Why is this bad? Competition is always good as it leads to innovation when compared to a monopoly". While this is true in a market I feel it does not reflect perfectly in the world of NPM and open source projects where anyone can contribute and influence decisions and directions taken by a specific piece of software/package. + +What is the consequence of this? Many abandoned, half-finished packages that need to be rewritten every now and then only to be left unmaintained again. Let the cycle repeat itself... + +## Bundle work! Not disperse it + +What if instead of reinventing the wheel over and over again we tune the wheel we have so that it becomes the best, most versatile wheel for everyone. Fragmentation is always a huge amount of work and leads to half-backed projects that always have some downsides. From that downside a new project is born and again, the cycle continues. + +## It's confusing for everyone and especially for beginners + +Ok so lets say I need to do some API calls in my node server. Of course I'm not going to use the native API because it's low level and not very readable. So what do I choose? Do I install `axios` or `node-fetch`, maybe `request`? What about `isomorphic-fetch`? Or `superagent`? + +This is a perfect example of the issue. They all try to solve the same very basic thing, in 5 different ways. Why cannot we have 1 or maybe 2? What if we could simply have the de facto library that does it all? + +Some time ago a friend coming from Python asked me that question and I could not give him an answer. He was visibly confused. In python there is no discussion really: you install `requests` and it will support 99.9% of use cases anyone will ever have. And if something is missing it will be added. Simple. + +Same thing with bundlers. `webpack`, `parcel`, `rollup` and now `snowpack`. Why?! Don't get me wrong, I'm sure all of them have some incredible smart engineering behind it, but what is the need of splitting the knowledge and not combine it to make something greater? + +I would just wish that JS people could start to converge on some best practices to ease the life of everyone developing in this ecosystem. diff --git a/src/content/blog/why-i-think-svelte-is-the-next-big-thing-a-reacts-lover-view.md b/src/content/blog/why-i-think-svelte-is-the-next-big-thing-a-reacts-lover-view.md new file mode 100644 index 0000000..13bd157 --- /dev/null +++ b/src/content/blog/why-i-think-svelte-is-the-next-big-thing-a-reacts-lover-view.md @@ -0,0 +1,192 @@ +--- +title: 'Why I think svelte is the next big thing: a reacts lover view' +date: '2020-10-29' +categories: + - 'coding' +tags: + - 'react' + - 'svelte' +coverImage: './images/alessandra-caretto-cAY9X4rPG3g-unsplash.jpg' +--- + +The first thing I thought when I heard there is this new framework called "Svelte" was: F\*\*\* no, please, not again. Why? Why do we need yet another framework ffs. React is amazing, don't try to reinvent the wheel. + +I was so wrong... + +So maybe one of you is thinking the same thing. Why [Svelte](https://svelte.dev/)? What is all the fuzz about? Well.... let me tell you πŸ™‚ + +This is not a tutorial, for that check the amazing [official tutorial](https://svelte.dev/tutorial/basics) which teaches you everything from the basics to more advanced stuff. + +
+ +![](images/alessandra-caretto-cAY9X4rPG3g-unsplash.jpg) + +
+ +Photo byΒ [Alessandra Caretto](https://unsplash.com/@alessandracaretto?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/s/photos/wheel?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +Why is there a wheel? Well quite simply: Svelte IS reinventing the wheel. +You see: what are most of the frameworks doing nowadays? They provide a way to code user interfaces with a component approach. This makes a lot of sense because we can reuse them, they are boxed items that stand for them self. Modularizing and splitting up concerns make big apps easier to write and afterwards maintain. + +What sets Svelte apart from a tecnical standpoint ist that it's foremost a compiler as opposed to a runtime like React or Vue. What does it mean? It means that all the reactivity is done at compile time and not in the browser. Svelte knows before hand what property can change and what doesn't. That's very powerful. + +## Easy of use and simplicity + +This in my opinion might be the biggest driver for svelte. The [official tutorial](https://svelte.dev/tutorial/basics) is great and well thought out. What stood out to me was how easy it was for non web people I know to get up and running in the matter of a day. +Simplicity is really hard however Svelte abtracts everything you need away and breaks it down to simple `html`, `js` and `css`. The learning curve is... well more like a flat line you just need to step on. + +## Taking the fun back to the web + +Personally the greatest feat Svelte achieves is staing out of the way while supporting you, the developer in everything you need to do. It is never in the way, it always makes your life easier. + +### Vue and React had a child + +It is very clear that Svelte was inspired by both React and Vue picking the best features of both worlds. This is probably why Svelte is so amazing. It has the luxury to build on years of experience of what works, and what does not. + +Inspired by React: + +- Components are JS Objects + +Inspired by Vue: + +- 2 Way binding +- Syling +- Making ecosystem first party +- Single file components + +Moslty "Unique" features: + +- Class toggles +- Stores + +## Styling + +Styling in react sucks. There is no way around it. In Vue that problem is solved by simply having scoped css built-in. No need for preprocessors, since we have CSS Variables and namespacing is no longer an issue. + +```svelte + + + + +

{title}

+ +

+ Virtues sexuality philosophy law chaos society evil strong self christianity sexuality truth. Revaluation convictions decrepit snare passion oneself decieve oneself. Right suicide grandeur fearful play. Right joy merciful transvaluation good truth derive evil contradict intentions. Self salvation faithful disgust marvelous transvaluation aversion. +

+ +Generate more! +``` + +No need to worry that our styles will affect some other stuff and we don't need to pass around classNames from other files. + +## Single File Components + +`.svelte` files are just like `.vue` files and they contain everything: code (`js`/`ts`), markup (`html`) and styling (`css`). No need to separate files for styling which bloats every React project that does not want to rely on css-in-js which in my opinion is just garbage. + +## 2 way binding + +This alone will save you tons of time and makes implementing stuff more intuitive and fun. + +```svelte + + + + +``` + +Yes, it will sync the UI to the variables, but both ways. In react you need to either use some 3rd party lib or juggle `value` and `onChange` events. + +This also works for parent - child props, so no need to write verbose update functions to be called from the children. Everyone hates those. + +## Stores + +So I truly hate Redux. It is the reason I've quit companies and unfortunately it is the way to go in react. In svelte we have build it stores and they are simply put: amazingly powerful and easy. + +```svelte +// store/todos.js +import { writable } from 'svelte/store' +import axios from 'axios' + +export const data = writable([]) + +export async function load() { + const { data: d } = await axios({ + method: 'get', + url: '/api/todos/', + }) + data.set(d) +} +``` + +```svelte + + +{#each $data as todo} + +{/each} +``` + +With a simple `$` in front of a store svelte will update the UI according to the store content. And we simply update it. No complex bootstrapping with context, dispatcher, acitons and so on. + +## Class toggles + +This is a feature I use all the time and it is just so useful. You can basically bind a value to toggle a css class, in this case `done`. No need to to string magic or, again, import some 3rd party module to manage our class names. + +```svelte + + + + +
+ Some task +
+``` + +## First party support + +Another thing that I cannot stand in React and Svelte does right in my opinion is that in the react world everything is basically up to the community. Even the typing! That means that while there is more choice in theory, in practise it leads to a fragmented unmaintained ecosystem. + +The Svelte is not afraid of incorporating tools from the community into the officials repos. I strongly believe that it is in everybodies interest that tooling and core functionality should always be first party code. Much like Vue does it. + +## Concluding thoughts + +First of all lets all thank Rich Harris which is the creator of Svelte. diff --git a/src/content/blog/write-cross-browser-extensions-without-the-tears.md b/src/content/blog/write-cross-browser-extensions-without-the-tears.md new file mode 100644 index 0000000..2b1401d --- /dev/null +++ b/src/content/blog/write-cross-browser-extensions-without-the-tears.md @@ -0,0 +1,211 @@ +--- +title: 'Write cross browser extensions without the tears' +date: '2021-12-28' +categories: + - 'coding' +tags: + - 'web-extension' +coverImage: './images/markus-spiske-8CWoXxaqGrs-unsplash-scaled.jpg' +--- + +Today I want to cover the process of developing browser extensions, from start to finish and submission to the chrome and firefox stores. +According to Apple the new Safari will have better support for extensions, but as for now (Dec. 2021) this is not the case so we will focus on Firefox and Chromium based browsers. + +This guide and the examples will be based on [Ora](https://github.com/cupcakearmy/ora), a web extension I wrote for both Firefox and Chrome. + +We will not touch on the [controversial](https://www.eff.org/deeplinks/2021/12/chrome-users-beware-manifest-v3-deceitful-and-threatening) manifest version 3. We'll stick to version 2. + +
+ +![](images/markus-spiske-8CWoXxaqGrs-unsplash-1024x683.jpg) + +
+ +Photo by [Markus Spiske](https://unsplash.com/@markusspiske?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/s/photos/lego?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +## The Basics + +Let's dive right in! A browser extensions is basically a series of small "websites". Logic is written in Javascript, styled in CSS, etc. This also means that we can use Vanilla JS, React, Svelte, whatever you prefer. + +There are mainly 3 types of content you need to know. + +1. **Content scripts** + These are files that you will be injected into existing tabs of the user. This can be `CSS` or `JS` files. They live on a tab level. +2. **Background scripts** + Here we have files that will run in the background, in a separate thread. They are not bound to any tab/window and have basically the same runtime as the browser itself. These are only `JS` scripts. +3. **Normal websites for your extension** + As we will se later, files inside your extensions will be served as if it was a server, so here you will code you settings page, dashboard and everything else. These are `HTML`, `CSS`, `JS`, images, etc. You name it. + Often this will be an options page, or a dashboard. + +Almost everything I learned about browser extensions I learned from the [MDN Docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions), they are amazing! If you are stuck this should be the first point to look. + +## The Manifest + +The first thing you'll need to know is a file called `manifest.json`. +Here you will define all the contents, permissions, icons, name, etc. It's the entry point to your app. It's where your high level configuration lives. + +```json +{ + "manifest_version": 2, + "name": "Ora", + "version": "0.8.1", + "description": "See how much time you spend on each website and set limits" +} +``` + +## Icons + +Every extensions has an icon. They are defined in the `manifest.json`. + +`[icons](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/icons)` is for the general icon, in the settings menu e.g. +`[browser_action](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_action)` is for the icon you'll see in the browser interface. Optionally you can specify `theme_icons` for dark mode support. + +As a general rule you can always specify different sizes, but in my experience the easiest method is to just use a good high res (256 or 512px) png. SVGs support is quite hit or miss, would not recommend it. + +Here I chose the same 512px png for both the extension and the interface. + +```json +{ + "manifest_version": 2, + ... + "icons": { + "512": "icons/watch.png" + }, + "browser_action": { + "default_icon": { + "512": "icons/watch.png" + }, + "default_title": "Ora Dashboard", + "theme_icons": [ + { + "light": "./icons/watch.png", + "dark": "./icons/watch-alt.png", + "size": 512 + } + ] + } +} +``` + +## Permission + +Browser extensions are really powerful, but need to request permissions to unlock that power. Most of them you'll find on your way but the most common are `` for injecting scripts/styles into every page. another common one would be `tabs` to be able to get information on all tabs. + +Try to keep permissions to the minimum possible and not request stuff you don't need. Permissions are listed when installing the extensions all at once, unlike on a phone where there is a prompt. + +For ora I needed `` to inject scripts to block a website when the time limit was reached, `tabs` to count the time spent on websites and `unlimitedStorage` & `storage` for saving all that data. + +```json +{ + "manifest_version": 2, + ... + "permissions": ["", "tabs", "unlimitedStorage", "storage"] +} +``` + +## Toolchain + +We all hate it, bundlers, framework setup, etc. I will try to give you the most basic, painless setup. And by far that is a setup using `[parcel.js](https://parceljs.org/)`. + +They have official [support for browser extensions](https://parceljs.org/recipes/web-extension/)! And it works really well. +I would write some little guide, but TBH it would just duplicate their docs so I will just point you to the [official documentation](https://parceljs.org/recipes/web-extension/) which explains the steps perfectly. + +The cool thing about parcel is that we use the `manifest.json` as entry point and just point to the sources we want to use inside of that. Parcel does the rest of the magic. + +```json +{ + "manifest_version": 2, + ... + "background": { + "scripts": ["./src/background/index.ts"] + }, + "content_scripts": [ + { + "matches": [""], + "run_at": "document_start", + "js": ["./src/content/index.ts"] + } + ] +} +``` + +As you can see above we just use `ts` typescript files in our manifest and parcel will compile them for us. And those typescript files can include React, Svelte, whatever framework and it will just work. Amazing 😍 + +For svelte you'll need to add the [typescript transformer](https://github.com/orlov-vo/parcel-transformer-svelte). React works out of the box. + +## Cross browser support without tears + +For this we need to thank the good folks over at Mozilla. They created [webextension-polyfill](https://github.com/mozilla/webextension-polyfill). + +```ts +import browser from 'webextension-polyfill' + +const tabs = await browser.tabs.query({ active: true }) +``` + +Just like that you will have code that will work in both Chrome and Firefox 99% of the time. This can be used in background scripts, client scripts and standalone pages. + +## Building + +```json +{ + ... + "scripts": { + "dev": "parcel watch --target dev --no-hmr ./manifest.json", + "build": "parcel build --target prod ./manifest.json", + "dist": "run-s clean build pack:*", + "pack:zip": "zip -r ./ora.zip dist/prod/*", + "pack:ff": "web-ext build -s dist/prod --overwrite-dest" + }, + "targets": { + "dev": { + "sourceMap": { + "inline": true, + "inlineSources": true + } + }, + "prod": {} + }, +} +``` + +This is the `package.json` of Ora. How is the extension built? + +1. Parcel bundler builds the raw web files, copies images, etc to the `./dist/prod` folder. +2. For Chrome we only need to zip it with `zip -r ./ora.zip dist/prod/*`. +3. For Firefox we use the mozilla own `[web-ext](https://github.com/mozilla/web-ext/)` tool with `web-ext build -s dist/prod --overwrite-dest`. + This results into a `web-ext-artifacts/ota-0.8.1.zip` file. + +## Distributing + +Now you can upload the normal zip file to the chrome web store after paying a one time 5$ fee and the web ext artefact to the mozilla add-on page. + +Mozilla also requires for minified apps (which this one is) to also submit sources. Just zip the source code (without node_modules) and upload them along the minified one. + +## Common use cases + +I also want to add a few notes on common use cases that every extensions uses. + +### Communication between content, background and other scripts. + +The `[postMessage](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendMessage)` API is the way to go. It's basically like IPC in the sense that you can send any serializable content from and to any of your scripts while listening for an answer. + +### Storage / Persistence + +Here you have a lot of options. The easiest way that works for most cases is using the `[storage](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage)` API. Here you get access to `local` and `sync` storage interfaces. `local` is.. well local and `sync` is synced between browsers if the user is logged in. Choose whatever suits you best. + +Another options, which is more powerful for more complex and data hungry is using something like `[Dexie.js](https://dexie.org/)` on top of IndexedDB. This is a full NoSQL kind of local database. A good options for data intensive extensions. + +### Shortcuts + +You can easily add keyboard shortcuts to your browser extension with the `[commands](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/commands)` entry in the `manifest.json`. This has the added benefit that browsers provide an interface where users can customize them. + +A little exception for opening the popup with a keyboard shortcut can be achieved with the `_execute_browser_action` command. + +## Final thoughts + +A browser extension is very fun project for a free Sunday and I'd recommend everyone to try it out, it's easier than you might think! diff --git a/src/content/blog/write-your-own-drone-plugin-from-scratch.md b/src/content/blog/write-your-own-drone-plugin-from-scratch.md new file mode 100644 index 0000000..a6cddce --- /dev/null +++ b/src/content/blog/write-your-own-drone-plugin-from-scratch.md @@ -0,0 +1,169 @@ +--- +title: 'Write your own drone plugin from scratch' +date: '2019-05-25' +categories: + - 'coding' +tags: + - 'cd' + - 'drone' + - 'guide' + - 'plugin' + - 'python' +coverImage: './images/asoggetti-418839-unsplash-scaled.jpg' +--- + +Recently Drone released it 1.0 version update. It came with a cleaner and more useful UI and a new structure for declaring the YAML drone pipelines files. Perfect time for writing our own drone plugin. πŸŽ‰ + +**TL;DR** Get the code: [https://github.com/CupCakeArmy/drone-deploy](https://github.com/CupCakeArmy/drone-deploy) + +
+ +![](images/asoggetti-418839-unsplash-1024x684.jpg) + +
+ +Photo byΒ [asoggetti](https://unsplash.com/photos/rSFxBGpnluw?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)Β onΒ [Unsplash](https://unsplash.com/search/photos/drone?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) + +
+ +
+ +This should be a starting point to guide you in the right direction towards writing your first drone plugin. **It's easier than it might seems**. Let's go πŸš€ + +#### What do we want to achieve? + +- Login with an ssh key from drone secrets +- copy files to a remote location +- execute remote commands + +## How are Drone Plugins written? + +Very simple actually: **drone plugins are nothing more than a docker image**. The process is as follows: + +1. The code gets mounted inside your image at \`/drone/src\` +2. You make slack api calls, deploy, whatever you want. + +Since the source is mounted inside a container you can use **whatever language you prefer**! Cool. For this small project I decided to go with Python, but you could use node, go, whatever runs inside Docker. + +First lets look how a plugin is defined inside the `.drone.yml` file. + +```yaml +kind: pipeline +name: default + +steps: + # build... + + - name: deploy + image: you/your-drone-plugin # public docker image + settings: + host: example.org + user: root + password: h4x0rz +``` + +**How do we get those variables?** Drone mounts them as environment variables inside the plugin container prefixed by `PLUGIN_`. +So `host: example.org` becomes `$PLUGIN_HOST=example.org` and so forth. + +###### In Python + +```python +def main(): + host = os.environ.get('PLUGIN_HOST') + port = os.environ.get('PLUGIN_PORT', 22) + user = os.environ.get('PLUGIN_USER') + password = os.environ.get('PLUGIN_PASSWORD') + key = os.environ.get('PLUGIN_KEY') + +main() +# example.org +# 22 +# root +# h4x0rz +# None +``` + +**How do I get secrets?** We need to use the `from_secret` syntax. Supposing you have saved your private key as a repository secret under `ssh_key` the example would look as follows: + +```yaml +- name: deploy + settings: + key: + from_secret: ssh_key +``` + +Easy πŸ‘Œ**What about arrays**? We want to copy a list of sources to the server. + +```yaml +- name: deploy + settings: + sources: + - ./public + - docker-compose.yml +``` + +This will result into `PLUGIN_SOURCES=./public,docker-compose.yml`. Basically we get a comma-separated-string. + +```yaml +# Takes a string, splits it at the comma and removes empty elements +def clean_array(s: str) -> List[str]: + return list(filter(None, s.split(','))) + +sources = clean_array(os.environ.get('PLUGIN_SOURCES', '')) +``` + +## Putting it all together + +```python +import paramiko + +def execute(c: SSHClient, cmd: str, path: str = None, env:dict = None) -> str: + if path is not None: + cmd = 'cd {}; {}'.format(path, cmd) + stdin, stdout, stderr = c.exec_command(cmd, environment=env) + return stdout.read().decode('utf-8').strip() + + +def main(): + + # Takes a string, splits it at the comma and removes empty elements + def clean_array(s: str) -> List[str]: + return list(filter(None, s.split(','))) + + host = os.environ.get('PLUGIN_HOST') + port = os.environ.get('PLUGIN_PORT', 22) + user = os.environ.get('PLUGIN_USER') + password = os.environ.get('PLUGIN_PASSWORD') + key = os.environ.get('PLUGIN_KEY') + target = os.environ.get('PLUGIN_TARGET') + commands = clean_array(os.environ.get('PLUGIN_COMMANDS', '')) + sources = clean_array(os.environ.get('PLUGIN_SOURCES', '')) + + ssh = paramiko.SSHClient() + try: + k = paramiko.RSAKey.from_private_key(io.StringIO(key)) + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(hostname=host, username=user, pkey=k, port=port, password=password) + + sftp = ssh.open_sftp() + try: + # Upload the files + # for file in file.... + sftp.put(archive_local, archive_remote) + finally: + sftp.close() + + for command in commands: + output = execute(ssh, command, target, envs) + print(command) + print(output) + + finally: + ssh.close() +``` + +This of course simplifies the code a lot, but should give a general idea how it's done. + +For a full fledged example check out the repo: [https://github.com/CupCakeArmy/drone-deploy](https://github.com/CupCakeArmy/drone-deploy) + +Enjoy! πŸ‘‹ diff --git a/src/content/config.ts b/src/content/config.ts new file mode 100644 index 0000000..9c24dfd --- /dev/null +++ b/src/content/config.ts @@ -0,0 +1,26 @@ +import { defineCollection, z } from 'astro:content' + +const blog = defineCollection({ + type: 'content', + // Type-check frontmatter using a schema + schema: ({ image }) => + z.object({ + title: z.string(), + description: z.string().optional(), + // Transform string to Date object + date: z.coerce.date(), + updatedDate: z.coerce.date().optional(), + coverImage: image().optional(), + tags: z.string().array().default([]), + }), +}) + +const page = defineCollection({ + type: 'content', + schema: z.object({ + title: z.string(), + date: z.coerce.date(), + }), +}) + +export const collections = { blog, page } diff --git a/src/content/images/about.webp b/src/content/images/about.webp new file mode 100644 index 0000000..4eb4947 --- /dev/null +++ b/src/content/images/about.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71a2c44560dcfe4896ee9da04a8c7f5116aab137707141bd9226b636a57e09af +size 211186 diff --git a/src/content/images/home.png b/src/content/images/home.png new file mode 100644 index 0000000..39b7f55 --- /dev/null +++ b/src/content/images/home.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:504713159b7a670ceaddcbac590e6c5cbbb0e3141c1e57bdba59636f39a3ec4c +size 4568347 diff --git a/src/content/page/about.mdx b/src/content/page/about.mdx new file mode 100644 index 0000000..0506d86 --- /dev/null +++ b/src/content/page/about.mdx @@ -0,0 +1,63 @@ +--- +title: 'About' +date: '2020-09-21' +--- +import SkillBar from '../../components/SkillBar.astro' +import AboutImage from '../../components/AboutImage.astro' + +Hi, I'm Nicco + +I've been doing programming things and "You do computer science right? Can you fix my printer?" for a while now. +My home is mainly the web, but I wander down many paths of computer science, wherever Ecosia and curiosity bring me that day. + +
+ + + +
+ +## Today + +I do a lot of Typescript, but I always try new things that come up on the interwebs and while most of them turn out to be hype, some are real gems. + +**A list of some "skills" (Pros)** + +- Frontend + - React, Vue & Svelte for Web apps, landing pages, etc. + - Typescript lover + - Electron multiplatform _macOS/Linux/Windows_ apps. + - Mobile apps for _iOS_ and _Android_, mostly in React Native. +- CI/CD + - I've written plug-ins for _Drone_ + - Docker multi stage build, etc. + - Pipelines for testing & deployments of various projects/apps + - Github actions +- Backend + - Serverless backends (in _AWS_) + - Postgres/Maria/Mysql/Mongo experience. + - Python server + - Go CLI utilities + - Web scraping +- Clouds I've deployed production services + - AWS + - GCP + +**And stuff I suck at (Cons)** + +- Testing, hate it. +- Love rewriting stuff from ground up. +- Probably much more... + +## History + +It all started when I turned the Google website pink with the dev tools in IE7 and thought I hacked google only to reload the website and discover that (of course) it was only local. + +That got me onto HTML & CSS and super vanilla JS. +Then of course came PHP it all it's glory which was the revelation for me. +I could write servers! OMG! Wait.. where to save that data? +"MySQL joined the chat" and along came some shady phpMyAdmin interfaces where we designed some databases and wrote SQL queries in plain strings always reminding ourselves to sanitise everything. +Remember kids: before our friendly Docker whale was a thing and we all installed XAMPP on our local machines with the (i believe still today) in beta Sublime Text 3. Good stuff. + +The rest... well I have a bad memory 🐘 + + diff --git a/src/content/page/privacy.md b/src/content/page/privacy.md new file mode 100644 index 0000000..9cafac5 --- /dev/null +++ b/src/content/page/privacy.md @@ -0,0 +1,28 @@ +--- +title: 'Privacy' +date: '2020-09-24' +--- + +### TLDR; + +- There is no personal data collected. +- No data is or will be passed to any third party. +- All the traffic data is publicly available [here](https://spectare.nicco.io/share/Xklie3UU/Nicco). + +## Privacy Policy + +### What data is collected + +We do not collect any personally identifiable information and anonymizes all data collected. Users cannot be identified and are never tracked across websites. Our analytics do contain any tracking code. + +### What is the data used for + +We aggregate statistics for analysing website usage. All this data is available publicly under the following link: [https://spectare.nicco.io/share/Xklie3UU/Nicco](https://spectare.nicco.io/share/Xklie3UU/Nicco). + +### Third Parties + +All the data is collected by us locally and never shared with any third party. + +### Contact + +For questions or issues please contact us at [privacy@nicco.io](mailto:privacy@nicco.io). diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..e16c13c --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/layouts/BlogPost.astro b/src/layouts/BlogPost.astro new file mode 100644 index 0000000..c90900e --- /dev/null +++ b/src/layouts/BlogPost.astro @@ -0,0 +1,36 @@ +--- +import type { CollectionEntry } from 'astro:content' +import FormattedDate from '../components/FormattedDate.astro' +import { Image } from 'astro:assets' +import Root from './Root.astro' + +type Props = CollectionEntry<'blog'>['data'] + +const { title, updatedDate, date, tags, coverImage } = Astro.props +--- + + +
+
+ {coverImage && } +
+
+
+
+ + { + updatedDate && ( +
+ Last updated on +
+ ) + } +
+

{title}

+ {tags.map((tag) => {tag})} +
+
+ +
+
+
diff --git a/src/layouts/PageWithTitle.astro b/src/layouts/PageWithTitle.astro new file mode 100644 index 0000000..d98b8ee --- /dev/null +++ b/src/layouts/PageWithTitle.astro @@ -0,0 +1,52 @@ +--- +import SpacedLetters from '../components/SpacedLetters.astro' +import Root from './Root.astro' + +export type Props = { + title: string + readable?: boolean + expanded?: boolean +} + +const { title, readable = false, expanded = true } = Astro.props +--- + + +
+

+ +

+
+ +
+
+
+ + diff --git a/src/layouts/Root.astro b/src/layouts/Root.astro new file mode 100644 index 0000000..bc6ec16 --- /dev/null +++ b/src/layouts/Root.astro @@ -0,0 +1,34 @@ +--- +import BaseHead from '../components/BaseHead.astro' +// import Footer from '../components/Footer.astro' +// import Header from '../components/Header.astro' +import Nav from '../components/Nav.astro' +--- + + + + + + + + +