From 1651e426344a9e55d685168e8057285fe9f54d6d Mon Sep 17 00:00:00 2001 From: goldbergyoni Date: Tue, 11 Jul 2023 13:20:46 +0000 Subject: [PATCH] deploy: 0f17bb8081f13671536085a1ac4edd4512a3573e --- 404.html | 8 ++-- ...bf0fd.1fefebe8.js => 39bbf0fd.73b84f3d.js} | 2 +- ...f3328.d078356d.js => 814f3328.34ea5b65.js} | 2 +- assets/js/93571253.701c974c.js | 1 + assets/js/93571253.ab9737d3.js | 1 - assets/js/a7023ddc.bdc2dd45.js | 1 - assets/js/a7023ddc.c09b8104.js | 1 + assets/js/b2f554cd.07401b26.js | 1 + assets/js/b2f554cd.d06d85c3.js | 1 - assets/js/d2a399e8.bd08fe86.js | 1 - assets/js/d2a399e8.cbf45e11.js | 1 + ...877fa.9df0ffd6.js => dac877fa.c7e0b7f7.js} | 2 +- assets/js/e25a597f.b2cc51c1.js | 1 - assets/js/e25a597f.b8006bb7.js | 1 + assets/js/ea907698.56ece2e6.js | 1 + assets/js/ea907698.850a16d9.js | 1 - .../js/{main.342a187c.js => main.f6bb4e9b.js} | 4 +- ...CENSE.txt => main.f6bb4e9b.js.LICENSE.txt} | 0 ...n.a20e2dde.js => runtime~main.1cad4a0f.js} | 2 +- blog/archive/index.html | 10 ++--- blog/atom.xml | 40 +++++++++---------- blog/index.html | 10 ++--- .../index.html | 10 ++--- blog/monorepo-backend/index.html | 12 +++--- .../index.html | 10 ++--- blog/practica-is-alive/index.html | 10 ++--- blog/practica-v0.0.6-is-alive/index.html | 10 ++--- blog/rss.xml | 24 +++++------ blog/tags/component-test/index.html | 10 ++--- blog/tags/decisions/index.html | 10 ++--- blog/tags/dotenv/index.html | 10 ++--- blog/tags/express/index.html | 10 ++--- blog/tags/fastify/index.html | 10 ++--- blog/tags/index.html | 10 ++--- blog/tags/integration/index.html | 10 ++--- blog/tags/monorepo/index.html | 10 ++--- blog/tags/nestjs/index.html | 10 ++--- blog/tags/nock/index.html | 10 ++--- blog/tags/node-js/index.html | 10 ++--- blog/tags/passport/index.html | 10 ++--- blog/tags/practica/index.html | 10 ++--- blog/tags/prisma/index.html | 10 ++--- blog/tags/supertest/index.html | 10 ++--- blog/tags/testing/index.html | 10 ++--- blog/tags/unit-test/index.html | 10 ++--- .../index.html | 10 ++--- .../contribution-long-guide/index.html | 8 ++-- .../contribution-short-guide/index.html | 8 ++-- contribution/release-checklist/index.html | 8 ++-- .../vendor-pick-guidelines/index.html | 8 ++-- decisions/configuration-library/index.html | 8 ++-- decisions/docker-base-image/index.html | 8 ++-- decisions/index.html | 8 ++-- decisions/monorepo/index.html | 8 ++-- decisions/openapi/index.html | 8 ++-- features/index.html | 8 ++-- index.html | 8 ++-- questions/index.html | 8 ++-- the-basics/coding-with-practica/index.html | 8 ++-- the-basics/getting-started-quickly/index.html | 8 ++-- the-basics/what-is-practica/index.html | 8 ++-- 61 files changed, 234 insertions(+), 234 deletions(-) rename assets/js/{39bbf0fd.1fefebe8.js => 39bbf0fd.73b84f3d.js} (52%) rename assets/js/{814f3328.d078356d.js => 814f3328.34ea5b65.js} (57%) create mode 100644 assets/js/93571253.701c974c.js delete mode 100644 assets/js/93571253.ab9737d3.js delete mode 100644 assets/js/a7023ddc.bdc2dd45.js create mode 100644 assets/js/a7023ddc.c09b8104.js create mode 100644 assets/js/b2f554cd.07401b26.js delete mode 100644 assets/js/b2f554cd.d06d85c3.js delete mode 100644 assets/js/d2a399e8.bd08fe86.js create mode 100644 assets/js/d2a399e8.cbf45e11.js rename assets/js/{dac877fa.9df0ffd6.js => dac877fa.c7e0b7f7.js} (52%) delete mode 100644 assets/js/e25a597f.b2cc51c1.js create mode 100644 assets/js/e25a597f.b8006bb7.js create mode 100644 assets/js/ea907698.56ece2e6.js delete mode 100644 assets/js/ea907698.850a16d9.js rename assets/js/{main.342a187c.js => main.f6bb4e9b.js} (99%) rename assets/js/{main.342a187c.js.LICENSE.txt => main.f6bb4e9b.js.LICENSE.txt} (100%) rename assets/js/{runtime~main.a20e2dde.js => runtime~main.1cad4a0f.js} (64%) diff --git a/404.html b/404.html index 1ecabaae..19490090 100644 --- a/404.html +++ b/404.html @@ -9,13 +9,13 @@ - - + +
Skip to main content

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

- - + + \ No newline at end of file diff --git a/assets/js/39bbf0fd.1fefebe8.js b/assets/js/39bbf0fd.73b84f3d.js similarity index 52% rename from assets/js/39bbf0fd.1fefebe8.js rename to assets/js/39bbf0fd.73b84f3d.js index b3e81970..07dd3356 100644 --- a/assets/js/39bbf0fd.1fefebe8.js +++ b/assets/js/39bbf0fd.73b84f3d.js @@ -1 +1 @@ -"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[4636],{3905:(e,t,a)=>{a.d(t,{Zo:()=>p,kt:()=>m});var r=a(7294);function i(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function n(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,r)}return a}function o(e){for(var t=1;t=0||(i[a]=e[a]);return i}(e,t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(i[a]=e[a])}return i}var s=r.createContext({}),c=function(e){var t=r.useContext(s),a=t;return e&&(a="function"==typeof e?e(t):o(o({},t),e)),a},p=function(e){var t=c(e.components);return r.createElement(s.Provider,{value:t},e.children)},u={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},d=r.forwardRef((function(e,t){var a=e.components,i=e.mdxType,n=e.originalType,s=e.parentName,p=l(e,["components","mdxType","originalType","parentName"]),d=c(a),m=i,h=d["".concat(s,".").concat(m)]||d[m]||u[m]||n;return a?r.createElement(h,o(o({ref:t},p),{},{components:a})):r.createElement(h,o({ref:t},p))}));function m(e,t){var a=arguments,i=t&&t.mdxType;if("string"==typeof e||i){var n=a.length,o=new Array(n);o[0]=d;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l.mdxType="string"==typeof e?e:i,o[1]=l;for(var c=2;c{a.r(t),a.d(t,{assets:()=>s,contentTitle:()=>o,default:()=>u,frontMatter:()=>n,metadata:()=>l,toc:()=>c});var r=a(7462),i=(a(7294),a(3905));const n={slug:"practica-v0.0.6-is-alive",date:"2022-12-10T10:00",hide_table_of_contents:!0,title:"Practica v0.0.6 is alive",authors:["goldbergyoni","razluvaton","danielgluskin","michaelsalomon"],tags:["node.js","express","practica","prisma"]},o=void 0,l={permalink:"/blog/practica-v0.0.6-is-alive",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/v0.6-is-alive/index.md",source:"@site/blog/v0.6-is-alive/index.md",title:"Practica v0.0.6 is alive",description:"Where is our focus now?",date:"2022-12-10T10:00:00.000Z",formattedDate:"December 10, 2022",tags:[{label:"node.js",permalink:"/blog/tags/node-js"},{label:"express",permalink:"/blog/tags/express"},{label:"practica",permalink:"/blog/tags/practica"},{label:"prisma",permalink:"/blog/tags/prisma"}],readingTime:1.47,hasTruncateMarker:!1,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Raz Luvaton",title:"Practica.js core maintainer",url:"https://github.com/rluvaton",imageURL:"https://avatars.githubusercontent.com/u/16746759?v=4",key:"razluvaton"},{name:"Daniel Gluskin",title:"Practica.js core maintainer",url:"https://github.com/DanielGluskin",imageURL:"https://avatars.githubusercontent.com/u/17989958?v=4",key:"danielgluskin"},{name:"Michael Salomon",title:"Practica.js core maintainer",url:"https://github.com/mikicho",imageURL:"https://avatars.githubusercontent.com/u/11459632?v=4",key:"michaelsalomon"}],frontMatter:{slug:"practica-v0.0.6-is-alive",date:"2022-12-10T10:00",hide_table_of_contents:!0,title:"Practica v0.0.6 is alive",authors:["goldbergyoni","razluvaton","danielgluskin","michaelsalomon"],tags:["node.js","express","practica","prisma"]},prevItem:{title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",permalink:"/blog/monorepo-backend"},nextItem:{title:"Is Prisma better than your 'traditional' ORM?",permalink:"/blog/is-prisma-better-than-your-traditional-orm"}},s={authorsImageUrls:[void 0,void 0,void 0,void 0]},c=[{value:"Where is our focus now?",id:"where-is-our-focus-now",level:2},{value:"What's new?",id:"whats-new",level:2},{value:"Request-level store",id:"request-level-store",level:3},{value:"Hardened .dockerfile",id:"hardened-dockerfile",level:3},{value:"Additional ORM option: Prisma",id:"additional-orm-option-prisma",level:3},{value:"Many small enhancements",id:"many-small-enhancements",level:3},{value:"Where do I start?",id:"where-do-i-start",level:2}],p={toc:c};function u(e){let{components:t,...a}=e;return(0,i.kt)("wrapper",(0,r.Z)({},p,a,{components:t,mdxType:"MDXLayout"}),(0,i.kt)("h2",{id:"where-is-our-focus-now"},"Where is our focus now?"),(0,i.kt)("p",null,"We work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback"),(0,i.kt)("h2",{id:"whats-new"},"What's new?"),(0,i.kt)("h3",{id:"request-level-store"},"Request-level store"),(0,i.kt)("p",null,"Every request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is 'request-id' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in ",(0,i.kt)("a",{parentName:"p",href:"https://nodejs.org/api/async_context.html"},"AsyncLocal")," for this task"),(0,i.kt)("h3",{id:"hardened-dockerfile"},"Hardened .dockerfile"),(0,i.kt)("p",null,"Although a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from ",(0,i.kt)("a",{parentName:"p",href:"https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/"},"this article")," and already apply 90% of the guidelines"),(0,i.kt)("h3",{id:"additional-orm-option-prisma"},"Additional ORM option: Prisma"),(0,i.kt)("p",null,"Prisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma"),(0,i.kt)("p",null,"Why did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this ",(0,i.kt)("a",{parentName:"p",href:"https://practica.dev/blog/is-prisma-better-than-your-traditional-orm/"},"blog post")),(0,i.kt)("h3",{id:"many-small-enhancements"},"Many small enhancements"),(0,i.kt)("p",null,"More than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more"),(0,i.kt)("h2",{id:"where-do-i-start"},"Where do I start?"),(0,i.kt)("p",null,"Definitely follow the ",(0,i.kt)("a",{parentName:"p",href:"https://practica.dev/the-basics/getting-started-quickly"},"getting started guide first")," and then read the guide ",(0,i.kt)("a",{parentName:"p",href:"https://practica.dev/the-basics/coding-with-practica"},"coding with practica")," to realize its full power and genuine value. We will be thankful to receive your feedback"))}u.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[4636],{3905:(e,t,a)=>{a.d(t,{Zo:()=>p,kt:()=>m});var r=a(7294);function i(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function n(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,r)}return a}function o(e){for(var t=1;t=0||(i[a]=e[a]);return i}(e,t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(i[a]=e[a])}return i}var s=r.createContext({}),c=function(e){var t=r.useContext(s),a=t;return e&&(a="function"==typeof e?e(t):o(o({},t),e)),a},p=function(e){var t=c(e.components);return r.createElement(s.Provider,{value:t},e.children)},u={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},d=r.forwardRef((function(e,t){var a=e.components,i=e.mdxType,n=e.originalType,s=e.parentName,p=l(e,["components","mdxType","originalType","parentName"]),d=c(a),m=i,h=d["".concat(s,".").concat(m)]||d[m]||u[m]||n;return a?r.createElement(h,o(o({ref:t},p),{},{components:a})):r.createElement(h,o({ref:t},p))}));function m(e,t){var a=arguments,i=t&&t.mdxType;if("string"==typeof e||i){var n=a.length,o=new Array(n);o[0]=d;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l.mdxType="string"==typeof e?e:i,o[1]=l;for(var c=2;c{a.r(t),a.d(t,{assets:()=>s,contentTitle:()=>o,default:()=>u,frontMatter:()=>n,metadata:()=>l,toc:()=>c});var r=a(7462),i=(a(7294),a(3905));const n={slug:"practica-v0.0.6-is-alive",date:"2022-12-10T10:00",hide_table_of_contents:!0,title:"Practica v0.0.6 is alive",authors:["goldbergyoni","razluvaton","danielgluskin","michaelsalomon"],tags:["node.js","express","practica","prisma"]},o=void 0,l={permalink:"/blog/practica-v0.0.6-is-alive",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/v0.6-is-alive/index.md",source:"@site/blog/v0.6-is-alive/index.md",title:"Practica v0.0.6 is alive",description:"Where is our focus now?",date:"2022-12-10T10:00:00.000Z",formattedDate:"December 10, 2022",tags:[{label:"node.js",permalink:"/blog/tags/node-js"},{label:"express",permalink:"/blog/tags/express"},{label:"practica",permalink:"/blog/tags/practica"},{label:"prisma",permalink:"/blog/tags/prisma"}],readingTime:1.47,hasTruncateMarker:!1,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Raz Luvaton",title:"Practica.js core maintainer",url:"https://github.com/rluvaton",imageURL:"https://avatars.githubusercontent.com/u/16746759?v=4",key:"razluvaton"},{name:"Daniel Gluskin",title:"Practica.js core maintainer",url:"https://github.com/DanielGluskin",imageURL:"https://avatars.githubusercontent.com/u/17989958?v=4",key:"danielgluskin"},{name:"Michael Salomon",title:"Practica.js core maintainer",url:"https://github.com/mikicho",imageURL:"https://avatars.githubusercontent.com/u/11459632?v=4",key:"michaelsalomon"}],frontMatter:{slug:"practica-v0.0.6-is-alive",date:"2022-12-10T10:00",hide_table_of_contents:!0,title:"Practica v0.0.6 is alive",authors:["goldbergyoni","razluvaton","danielgluskin","michaelsalomon"],tags:["node.js","express","practica","prisma"]},prevItem:{title:"Testing the dark scenarios of your Node.js application",permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application"},nextItem:{title:"Is Prisma better than your 'traditional' ORM?",permalink:"/blog/is-prisma-better-than-your-traditional-orm"}},s={authorsImageUrls:[void 0,void 0,void 0,void 0]},c=[{value:"Where is our focus now?",id:"where-is-our-focus-now",level:2},{value:"What's new?",id:"whats-new",level:2},{value:"Request-level store",id:"request-level-store",level:3},{value:"Hardened .dockerfile",id:"hardened-dockerfile",level:3},{value:"Additional ORM option: Prisma",id:"additional-orm-option-prisma",level:3},{value:"Many small enhancements",id:"many-small-enhancements",level:3},{value:"Where do I start?",id:"where-do-i-start",level:2}],p={toc:c};function u(e){let{components:t,...a}=e;return(0,i.kt)("wrapper",(0,r.Z)({},p,a,{components:t,mdxType:"MDXLayout"}),(0,i.kt)("h2",{id:"where-is-our-focus-now"},"Where is our focus now?"),(0,i.kt)("p",null,"We work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback"),(0,i.kt)("h2",{id:"whats-new"},"What's new?"),(0,i.kt)("h3",{id:"request-level-store"},"Request-level store"),(0,i.kt)("p",null,"Every request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is 'request-id' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in ",(0,i.kt)("a",{parentName:"p",href:"https://nodejs.org/api/async_context.html"},"AsyncLocal")," for this task"),(0,i.kt)("h3",{id:"hardened-dockerfile"},"Hardened .dockerfile"),(0,i.kt)("p",null,"Although a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from ",(0,i.kt)("a",{parentName:"p",href:"https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/"},"this article")," and already apply 90% of the guidelines"),(0,i.kt)("h3",{id:"additional-orm-option-prisma"},"Additional ORM option: Prisma"),(0,i.kt)("p",null,"Prisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma"),(0,i.kt)("p",null,"Why did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this ",(0,i.kt)("a",{parentName:"p",href:"https://practica.dev/blog/is-prisma-better-than-your-traditional-orm/"},"blog post")),(0,i.kt)("h3",{id:"many-small-enhancements"},"Many small enhancements"),(0,i.kt)("p",null,"More than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more"),(0,i.kt)("h2",{id:"where-do-i-start"},"Where do I start?"),(0,i.kt)("p",null,"Definitely follow the ",(0,i.kt)("a",{parentName:"p",href:"https://practica.dev/the-basics/getting-started-quickly"},"getting started guide first")," and then read the guide ",(0,i.kt)("a",{parentName:"p",href:"https://practica.dev/the-basics/coding-with-practica"},"coding with practica")," to realize its full power and genuine value. We will be thankful to receive your feedback"))}u.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/814f3328.d078356d.js b/assets/js/814f3328.34ea5b65.js similarity index 57% rename from assets/js/814f3328.d078356d.js rename to assets/js/814f3328.34ea5b65.js index 418cfe1d..2f756bf4 100644 --- a/assets/js/814f3328.d078356d.js +++ b/assets/js/814f3328.34ea5b65.js @@ -1 +1 @@ -"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[2535],{5641:t=>{t.exports=JSON.parse('{"title":"Recent posts","items":[{"title":"Testing the dark scenarios of your Node.js application","permalink":"/blog/testing-the-dark-scenarios-of-your-nodejs-application"},{"title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","permalink":"/blog/monorepo-backend"},{"title":"Practica v0.0.6 is alive","permalink":"/blog/practica-v0.0.6-is-alive"},{"title":"Is Prisma better than your \'traditional\' ORM?","permalink":"/blog/is-prisma-better-than-your-traditional-orm"},{"title":"Popular Node.js patterns and tools to re-consider","permalink":"/blog/popular-nodejs-pattern-and-tools-to-reconsider"}]}')}}]); \ No newline at end of file +"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[2535],{5641:t=>{t.exports=JSON.parse('{"title":"Recent posts","items":[{"title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","permalink":"/blog/monorepo-backend"},{"title":"Testing the dark scenarios of your Node.js application","permalink":"/blog/testing-the-dark-scenarios-of-your-nodejs-application"},{"title":"Practica v0.0.6 is alive","permalink":"/blog/practica-v0.0.6-is-alive"},{"title":"Is Prisma better than your \'traditional\' ORM?","permalink":"/blog/is-prisma-better-than-your-traditional-orm"},{"title":"Popular Node.js patterns and tools to re-consider","permalink":"/blog/popular-nodejs-pattern-and-tools-to-reconsider"}]}')}}]); \ No newline at end of file diff --git a/assets/js/93571253.701c974c.js b/assets/js/93571253.701c974c.js new file mode 100644 index 00000000..1802260c --- /dev/null +++ b/assets/js/93571253.701c974c.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[1265],{3905:(e,t,n)=>{n.d(t,{Zo:()=>h,kt:()=>p});var a=n(7294);function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function r(e){for(var t=1;t=0||(s[n]=e[n]);return s}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}var l=a.createContext({}),c=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},h=function(e){var t=c(e.components);return a.createElement(l.Provider,{value:t},e.children)},u={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},d=a.forwardRef((function(e,t){var n=e.components,s=e.mdxType,o=e.originalType,l=e.parentName,h=i(e,["components","mdxType","originalType","parentName"]),d=c(n),p=s,m=d["".concat(l,".").concat(p)]||d[p]||u[p]||o;return n?a.createElement(m,r(r({ref:t},h),{},{components:n})):a.createElement(m,r({ref:t},h))}));function p(e,t){var n=arguments,s=t&&t.mdxType;if("string"==typeof e||s){var o=n.length,r=new Array(o);r[0]=d;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i.mdxType="string"==typeof e?e:s,r[1]=i;for(var c=2;c{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>u,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var a=n(7462),s=(n(7294),n(3905));const o={slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},r=void 0,i={permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/crucial-tests/index.md",source:"@site/blog/crucial-tests/index.md",title:"Testing the dark scenarios of your Node.js application",description:"Where the dead-bodies are covered",date:"2023-07-07T11:00:00.000Z",formattedDate:"July 7, 2023",tags:[{label:"node.js",permalink:"/blog/tags/node-js"},{label:"testing",permalink:"/blog/tags/testing"},{label:"component-test",permalink:"/blog/tags/component-test"},{label:"fastify",permalink:"/blog/tags/fastify"},{label:"unit-test",permalink:"/blog/tags/unit-test"},{label:"integration",permalink:"/blog/tags/integration"},{label:"nock",permalink:"/blog/tags/nock"}],readingTime:20.3,hasTruncateMarker:!1,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Raz Luvaton",title:"Practica.js core maintainer",url:"https://github.com/rluvaton",imageURL:"https://avatars.githubusercontent.com/u/16746759?v=4",key:"razluvaton"}],frontMatter:{slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},prevItem:{title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",permalink:"/blog/monorepo-backend"},nextItem:{title:"Practica v0.0.6 is alive",permalink:"/blog/practica-v0.0.6-is-alive"}},l={authorsImageUrls:[void 0,void 0]},c=[{value:"Where the dead-bodies are covered",id:"where-the-dead-bodies-are-covered",level:2},{value:"Test Examples",id:"test-examples",level:2},{value:"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test",id:"\ufe0f-the-zombie-process-test",level:2},{value:"\ud83d\udc40 The observability test",id:"-the-observability-test",level:2},{value:"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code",id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code",level:2},{value:"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all",id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all",level:2},{value:"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much",id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much",level:2},{value:"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out",id:"-the-slow-collaborator-test---when-the-other-http-service-times-out",level:2},{value:"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation",id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation",level:2},{value:"\ud83d\udce6 Test the package as a consumer",id:"-test-the-package-as-a-consumer",level:2},{value:"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug",id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug",level:2},{value:"Even more ideas",id:"even-more-ideas",level:2},{value:"It's not just ideas, it a whole new mindset",id:"its-not-just-ideas-it-a-whole-new-mindset",level:2}],h={toc:c};function u(e){let{components:t,...o}=e;return(0,s.kt)("wrapper",(0,a.Z)({},h,o,{components:t,mdxType:"MDXLayout"}),(0,s.kt)("h2",{id:"where-the-dead-bodies-are-covered"},"Where the dead-bodies are covered"),(0,s.kt)("p",null,"This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked"),(0,s.kt)("p",null,"Some context first: How do we test a modern backend? With ",(0,s.kt)("a",{parentName:"p",href:"https://ritesh-kapoor.medium.com/testing-automation-what-are-pyramids-and-diamonds-67494fec7c55"},"the testing diamond"),", of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices"},"a guide with 50 best practices for integration tests in Node.js")),(0,s.kt)("p",null,"But there is a pitfall: most developers write ",(0,s.kt)("em",{parentName:"p"},"only")," semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime"),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"The hidden corners",src:n(8933).Z,width:"900",height:"521"})),(0,s.kt)("p",null,"Here are a handful of examples that might open your mind to a whole new class of risks and tests"),(0,s.kt)("p",null,(0,s.kt)("em",{parentName:"p"},"Side note: I've just released a comprehensive testing course that I've been working on for two years. \ud83c\udf81 It's now on sale, but only for the month of July. Check it out at ",(0,s.kt)("a",{parentName:"em",href:"https://testjavascript.com/"},"testjavascript.com"))),(0,s.kt)("h2",{id:"test-examples"},(0,s.kt)("strong",{parentName:"h2"},"Test Examples")),(0,s.kt)("h2",{id:"\ufe0f-the-zombie-process-test"},"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what? -")," In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see ",(0,s.kt)("a",{parentName:"p",href:"https://komodor.com/learn/kubernetes-readiness-probes-a-practical-guide/#:~:text=A%20readiness%20probe%20allows%20Kubernetes,on%20deletion%20of%20a%20pod."},"readiness probe"),"). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, api.js:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// A common express server initialization\nconst startWebServer = () => {\n return new Promise((resolve, reject) => {\n try {\n // A typical Express setup\n expressApp = express();\n defineRoutes(expressApp); // a function that defines all routes\n expressApp.listen(process.env.WEB_SERVER_PORT);\n } catch (error) {\n //log here, fire a metric, maybe even retry and finally:\n process.exit();\n }\n });\n};\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function\nconst sinon = require('sinon'); // a mocking library\n\ntest('When an error happens during the startup phase, then the process exits', async () => {\n // Arrange\n const processExitListener = sinon.stub(process, 'exit');\n // \ud83d\udc47 Choose a function that is part of the initialization phase and make it fail\n sinon\n .stub(routes, 'defineRoutes')\n .throws(new Error('Cant initialize connection'));\n\n // Act\n await api.startWebServer();\n\n // Assert\n expect(processExitListener.called).toBe(true);\n});\n")),(0,s.kt)("h2",{id:"-the-observability-test"},"\ud83d\udc40 The observability test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error ",(0,s.kt)("strong",{parentName:"p"},"correctly observable"),". In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, ",(0,s.kt)("em",{parentName:"p"},"including stack trace"),", cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When exception is throw during request, Then logger reports the mandatory fields', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n status: 'approved',\n };\n const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');\n sinon\n .stub(OrderRepository.prototype, 'addOrder')\n .rejects(new AppError('saving-failed', 'Order could not be saved', 500));\n const loggerDouble = sinon.stub(logger, 'error');\n\n //Act\n await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n expect(loggerDouble).toHaveBeenCalledWith({\n name: 'saving-failed',\n status: 500,\n stack: expect.any(String),\n message: expect.any(String),\n });\n expect(\n metricsExporterDouble).toHaveBeenCalledWith('error', {\n errorName: 'example-error',\n })\n});\n")),(0,s.kt)("h2",{id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code"},"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, ",(0,s.kt)("strong",{parentName:"p"},"hopefully if your code subscribed"),". How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:"),(0,s.kt)("p",null,"researches says that, rejection"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {\n //Arrange\n const loggerDouble = sinon.stub(logger, 'error');\n const processExitListener = sinon.stub(process, 'exit');\n const errorToThrow = new Error('An error that wont be caught \ud83d\ude33');\n\n //Act\n process.emit('uncaughtException', errorToThrow); //\ud83d\udc48 Where the magic is\n\n // Assert\n expect(processExitListener.called).toBe(false);\n expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);\n});\n")),(0,s.kt)("h2",{id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all"},"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n mode: 'draft',\n externalIdentifier: uuid(), //no existing record has this value\n };\n\n //Act\n const { status: addingHTTPStatus } = await axiosAPIClient.post(\n '/order',\n orderToAdd\n );\n\n //Assert\n const { status: fetchingHTTPStatus } = await axiosAPIClient.get(\n `/order/externalIdentifier/${orderToAdd.externalIdentifier}`\n ); // Trying to get the order that should have failed\n expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({\n addingHTTPStatus: 400,\n fetchingHTTPStatus: 404,\n });\n // \ud83d\udc46 Check that no such record exists\n});\n")),(0,s.kt)("h2",{id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much"},"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When deleting an existing order, Then it should NOT be retrievable', async () => {\n // Arrange\n const orderToDelete = {\n userId: 1,\n productId: 2,\n };\n const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data\n .id; // We will delete this soon\n const orderNotToBeDeleted = orderToDelete;\n const notDeletedOrder = (\n await axiosAPIClient.post('/order', orderNotToBeDeleted)\n ).data.id; // We will not delete this\n\n // Act\n await axiosAPIClient.delete(`/order/${deletedOrder}`);\n\n // Assert\n const { status: getDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${deletedOrder}`\n );\n const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${notDeletedOrder}`\n );\n expect(getNotDeletedOrderStatus).toBe(200);\n expect(getDeletedOrderStatus).toBe(404);\n});\n")),(0,s.kt)("h2",{id:"-the-slow-collaborator-test---when-the-other-http-service-times-out"},"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock")," or ",(0,s.kt)("a",{parentName:"p",href:"https://wiremock.org/"},"wiremock"),". These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available ",(0,s.kt)("strong",{parentName:"p"},"in production"),", what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use ",(0,s.kt)("a",{parentName:"p",href:"https://sinonjs.org/releases/latest/fake-timers/"},"fake timers")," and trick the system into believing as few seconds passed in a single tick. If you're using ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock"),", it offers an interesting feature to simulate timeouts ",(0,s.kt)("strong",{parentName:"p"},"quickly"),": the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// In this example, our code accepts new Orders and while processing them approaches the Users Microservice\ntest('When users service times out, then return 503 (option 1 with fake timers)', async () => {\n //Arrange\n const clock = sinon.useFakeTimers();\n config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls\n nock(`${config.userServiceURL}/user/`)\n .get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout \ud83d\udc46\n .reply(200);\n const loggerDouble = sinon.stub(logger, 'error');\n const orderToAdd = {\n userId: 1,\n productId: 2,\n mode: 'approved',\n };\n\n //Act\n // \ud83d\udc47try to add new order which should fail due to User service not available\n const response = await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n // \ud83d\udc47At least our code does its best given this situation\n expect(response.status).toBe(503);\n expect(loggerDouble.lastCall.firstArg).toMatchObject({\n name: 'user-service-not-available',\n stack: expect.any(String),\n message: expect.any(String),\n });\n});\n")),(0,s.kt)("h2",{id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation"},"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why"),(0,s.kt)("p",null,"When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. ",(0,s.kt)("a",{parentName:"p",href:"https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-using-purge-queue.html"},"SQS demand 60 seconds")," to purge queues), to name a few challenges that you won't find when dealing with real DB"),(0,s.kt)("p",null,"Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/m-radzikowski/aws-sdk-client-mock"},"this one for SQS")," and you can code one ",(0,s.kt)("strong",{parentName:"p"},"easily")," yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("ol",null,(0,s.kt)("li",{parentName:"ol"},"Create a fake message queue that does almost nothing but record calls, see full example here")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class FakeMessageQueueProvider extends EventEmitter {\n // Implement here\n\n publish(message) {}\n\n consume(queueName, callback) {}\n}\n")),(0,s.kt)("ol",{start:2},(0,s.kt)("li",{parentName:"ol"},"Make your message queue client accept real or fake provider")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n // Pass to it a fake or real message queue\n constructor(customMessageQueueProvider) {}\n\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // Simple implementation can be found here:\n // https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js\n}\n")),(0,s.kt)("ol",{start:3},(0,s.kt)("li",{parentName:"ol"},"Expose a convenient function that tells when certain calls where made")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // \ud83d\udc47\n waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise\n}\n")),(0,s.kt)("ol",{start:4},(0,s.kt)("li",{parentName:"ol"},"The test is now short, flat and expressive \ud83d\udc47")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');\nconst MessageQueueClient = require('./libs/message-queue-client');\nconst newOrderService = require('./domain/newOrderService');\n\ntest('When a poisoned message arrives, then it is being rejected back', async () => {\n // Arrange\n const messageWithInvalidSchema = { nonExistingProperty: 'invalid\u274c' };\n const messageQueueClient = new MessageQueueClient(\n new FakeMessageQueueProvider()\n );\n // Subscribe to new messages and passing the handler function\n messageQueueClient.consume('orders.new', newOrderService.addOrder);\n\n // Act\n await messageQueueClient.publish('orders.new', messageWithInvalidSchema);\n // Now all the layers of the app will get stretched \ud83d\udc46, including logic and message queue libraries\n\n // Assert\n await messageQueueClient.waitFor('reject', { howManyTimes: 1 });\n // \ud83d\udc46 This tells us that eventually our code asked the message queue client to reject this poisoned message\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/recipes/message-queue/fake-message-queue.test.js"},"is here")),(0,s.kt)("h2",{id:"-test-the-package-as-a-consumer"},"\ud83d\udce6 Test the package as a consumer"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts ",(0,s.kt)("em",{parentName:"p"},"that were built"),". See the mismatch here? ",(0,s.kt)("em",{parentName:"p"},"after")," running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,"Consider the following scenario, you're developing a library, and you wrote this code:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// index.js\nexport * from './calculate.js';\n\n// calculate.js \ud83d\udc48\nexport function calculate() {\n return 1;\n}\n")),(0,s.kt)("p",null,"Then some tests:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"import { calculate } from './index.js';\n\ntest('should return 1', () => {\n expect(calculate()).toBe(1);\n})\n\n\u2705 All tests pass \ud83c\udf8a\n")),(0,s.kt)("p",null,"Finally configure the package.json:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json5"},'{\n // ....\n "files": [\n "index.js"\n ]\n}\n')),(0,s.kt)("p",null,"See, 100% coverage, all tests pass locally and in the CI \u2705, it just won't work in production \ud83d\udc79. Why? because you forgot to include the ",(0,s.kt)("inlineCode",{parentName:"p"},"calculate.js")," in the package.json ",(0,s.kt)("inlineCode",{parentName:"p"},"files")," array \ud83d\udc46"),(0,s.kt)("p",null,"What can we do instead? we can test the library as ",(0,s.kt)("em",{parentName:"p"},"its end-users"),". How? publish the package to a local registry like ",(0,s.kt)("a",{parentName:"p",href:"https://verdaccio.org/"},"verdaccio"),", let the tests install and approach the ",(0,s.kt)("em",{parentName:"p"},"published")," code. Sounds troublesome? judge yourself \ud83d\udc47"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// global-setup.js\n\n// 1. Setup the in-memory NPM registry, one function that's it! \ud83d\udd25\nawait setupVerdaccio();\n\n// 2. Building our package \nawait exec('npm', ['run', 'build'], {\n cwd: packagePath,\n});\n\n// 3. Publish it to the in-memory registry\nawait exec('npm', ['publish', '--registry=http://localhost:4873'], {\n cwd: packagePath,\n});\n\n// 4. Installing it in the consumer directory\nawait exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {\n cwd: consumerPath,\n});\n\n// Test file in the consumerPath\n\n// 5. Test the package \ud83d\ude80\ntest(\"should succeed\", async () => {\n const { fn1 } = await import('my-package');\n\n expect(fn1()).toEqual(1);\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/rluvaton/e2e-verdaccio-example"},"is here")),(0,s.kt)("p",null,"What else this technique can be useful for?"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that"),(0,s.kt)("li",{parentName:"ul"},"You want to test ESM and CJS consumers"),(0,s.kt)("li",{parentName:"ul"},"If you have CLI application you can test it like your users"),(0,s.kt)("li",{parentName:"ul"},"Making sure all the voodoo magic in that babel file is working as expected")),(0,s.kt)("h2",{id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug"},"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -"),' Quite confidently I\'m sure that almost no team test their OpenAPI correctness. "It\'s just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.'),(0,s.kt)("p",null,"Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., ",(0,s.kt)("a",{parentName:"p",href:"https://pact.io"},"PACT"),"), there are also leaner approaches that gets you covered ",(0,s.kt)("em",{parentName:"p"},"easily and quickly")," (at the price of covering less risks)."),(0,s.kt)("p",null,"The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, an API throw a new error status")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"if (doesOrderCouponAlreadyExist) {\n throw new AppError('duplicated-coupon', { httpStatus: 409 });\n}\n")),(0,s.kt)("p",null,"The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json"},'"responses": {\n "200": {\n "description": "successful",\n }\n ,\n "400": {\n "description": "Invalid ID",\n "content": {}\n },// No 409 in this list\ud83d\ude32\ud83d\udc48\n}\n\n')),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const jestOpenAPI = require('jest-openapi');\njestOpenAPI('../openapi.json');\n\ntest('When an order with duplicated coupon is added , then 409 error should get returned', async () => {\n // Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n couponId: uuid(),\n };\n await axiosAPIClient.post('/order', orderToAdd);\n\n // Act\n // We're adding the same coupon twice \ud83d\udc47\n const receivedResponse = await axios.post('/order', orderToAdd);\n\n // Assert;\n expect(receivedResponse.status).toBe(409);\n expect(res).toSatisfyApiSpec();\n // This \ud83d\udc46 will throw if the API response, body or status, is different that was it stated in the OpenAPI\n});\n")),(0,s.kt)("p",null,"Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"beforeAll(() => {\n axios.interceptors.response.use((response) => {\n expect(response.toSatisfyApiSpec());\n // With this \ud83d\udc46, add nothing to the tests - each will fail if the response deviates from the docs\n });\n});\n")),(0,s.kt)("h2",{id:"even-more-ideas"},"Even more ideas"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Test readiness and health routes"),(0,s.kt)("li",{parentName:"ul"},"Test message queue connection failures"),(0,s.kt)("li",{parentName:"ul"},"Test JWT and JWKS failures"),(0,s.kt)("li",{parentName:"ul"},"Test security-related things like CSRF tokens"),(0,s.kt)("li",{parentName:"ul"},"Test your HTTP client retry mechanism (very easy with nock)"),(0,s.kt)("li",{parentName:"ul"},"Test that the DB migration succeed and the new code can work with old records format"),(0,s.kt)("li",{parentName:"ul"},"Test DB connection disconnects"),(0,s.kt)("li",{parentName:"ul"},"You may find many more examples at my fresh new testing course - ",(0,s.kt)("a",{parentName:"li",href:"https://testjavascript.com"},"testjavascript.com"))),(0,s.kt)("h2",{id:"its-not-just-ideas-it-a-whole-new-mindset"},"It's not just ideas, it a whole new mindset"),(0,s.kt)("p",null,"The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"My new online testing course -")," If you're intrigued with beyond the basics testing patterns, ]consider my online course which was just launched and is \ud83c\udf81 on sale for 30 days (July 2023)](",(0,s.kt)("a",{parentName:"p",href:"https://testjavascript.com"},"https://testjavascript.com"),")"))}u.isMDXComponent=!0},8933:(e,t,n)=>{n.d(t,{Z:()=>a});const a=n.p+"assets/images/the-hidden-corners-44855c2e5d9184502e1dc72b07d53cef.png"}}]); \ No newline at end of file diff --git a/assets/js/93571253.ab9737d3.js b/assets/js/93571253.ab9737d3.js deleted file mode 100644 index 7f4fb7ab..00000000 --- a/assets/js/93571253.ab9737d3.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[1265],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>p});var a=n(7294);function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function r(e){for(var t=1;t=0||(s[n]=e[n]);return s}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}var l=a.createContext({}),c=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},u=function(e){var t=c(e.components);return a.createElement(l.Provider,{value:t},e.children)},h={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},d=a.forwardRef((function(e,t){var n=e.components,s=e.mdxType,o=e.originalType,l=e.parentName,u=i(e,["components","mdxType","originalType","parentName"]),d=c(n),p=s,m=d["".concat(l,".").concat(p)]||d[p]||h[p]||o;return n?a.createElement(m,r(r({ref:t},u),{},{components:n})):a.createElement(m,r({ref:t},u))}));function p(e,t){var n=arguments,s=t&&t.mdxType;if("string"==typeof e||s){var o=n.length,r=new Array(o);r[0]=d;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i.mdxType="string"==typeof e?e:s,r[1]=i;for(var c=2;c{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var a=n(7462),s=(n(7294),n(3905));const o={slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},r=void 0,i={permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/crucial-tests/index.md",source:"@site/blog/crucial-tests/index.md",title:"Testing the dark scenarios of your Node.js application",description:"Where the dead-bodies are covered",date:"2023-07-07T11:00:00.000Z",formattedDate:"July 7, 2023",tags:[{label:"node.js",permalink:"/blog/tags/node-js"},{label:"testing",permalink:"/blog/tags/testing"},{label:"component-test",permalink:"/blog/tags/component-test"},{label:"fastify",permalink:"/blog/tags/fastify"},{label:"unit-test",permalink:"/blog/tags/unit-test"},{label:"integration",permalink:"/blog/tags/integration"},{label:"nock",permalink:"/blog/tags/nock"}],readingTime:19.875,hasTruncateMarker:!1,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Raz Luvaton",title:"Practica.js core maintainer",url:"https://github.com/rluvaton",imageURL:"https://avatars.githubusercontent.com/u/16746759?v=4",key:"razluvaton"}],frontMatter:{slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},nextItem:{title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",permalink:"/blog/monorepo-backend"}},l={authorsImageUrls:[void 0,void 0]},c=[{value:"Where the dead-bodies are covered",id:"where-the-dead-bodies-are-covered",level:2},{value:"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test",id:"\ufe0f-the-zombie-process-test",level:2},{value:"\ud83d\udc40 The observability test",id:"-the-observability-test",level:2},{value:"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code",id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code",level:2},{value:"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all",id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all",level:2},{value:"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much",id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much",level:2},{value:"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out",id:"-the-slow-collaborator-test---when-the-other-http-service-times-out",level:2},{value:"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation",id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation",level:2},{value:"\ud83d\udce6 Test the package as a consumer",id:"-test-the-package-as-a-consumer",level:2},{value:"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug",id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug",level:2},{value:"Even more ideas",id:"even-more-ideas",level:2},{value:"It's not just ideas, it a whole new mindset",id:"its-not-just-ideas-it-a-whole-new-mindset",level:2}],u={toc:c};function h(e){let{components:t,...o}=e;return(0,s.kt)("wrapper",(0,a.Z)({},u,o,{components:t,mdxType:"MDXLayout"}),(0,s.kt)("h2",{id:"where-the-dead-bodies-are-covered"},"Where the dead-bodies are covered"),(0,s.kt)("p",null,"This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked"),(0,s.kt)("p",null,"Some context first: How do we test a modern backend? With ",(0,s.kt)("a",{parentName:"p",href:"https://ritesh-kapoor.medium.com/testing-automation-what-are-pyramids-and-diamonds-67494fec7c55"},"the testing diamond"),", of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices"},"a guide with 50 best practices for integration tests in Node.js")),(0,s.kt)("p",null,"But there is a pitfall: most developers write ",(0,s.kt)("em",{parentName:"p"},"only")," semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime"),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"The hidden corners",src:n(8933).Z,width:"900",height:"521"})),(0,s.kt)("p",null,"Here are a handful of examples that might open your mind to a whole new class of risks and tests"),(0,s.kt)("h2",{id:"\ufe0f-the-zombie-process-test"},"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what? -")," In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see ",(0,s.kt)("a",{parentName:"p",href:"https://komodor.com/learn/kubernetes-readiness-probes-a-practical-guide/#:~:text=A%20readiness%20probe%20allows%20Kubernetes,on%20deletion%20of%20a%20pod."},"readiness probe"),"). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, api.js:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// A common express server initialization\nconst startWebServer = () => {\n return new Promise((resolve, reject) => {\n try {\n // A typical Express setup\n expressApp = express();\n defineRoutes(expressApp); // a function that defines all routes\n expressApp.listen(process.env.WEB_SERVER_PORT);\n } catch (error) {\n //log here, fire a metric, maybe even retry and finally:\n process.exit();\n }\n });\n};\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function\nconst sinon = require('sinon'); // a mocking library\n\ntest('When an error happens during the startup phase, then the process exits', async () => {\n // Arrange\n const processExitListener = sinon.stub(process, 'exit');\n // \ud83d\udc47 Choose a function that is part of the initialization phase and make it fail\n sinon\n .stub(routes, 'defineRoutes')\n .throws(new Error('Cant initialize connection'));\n\n // Act\n await api.startWebServer();\n\n // Assert\n expect(processExitListener.called).toBe(true);\n});\n")),(0,s.kt)("h2",{id:"-the-observability-test"},"\ud83d\udc40 The observability test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error ",(0,s.kt)("strong",{parentName:"p"},"correctly observable"),". In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, ",(0,s.kt)("em",{parentName:"p"},"including stack trace"),", cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When exception is throw during request, Then logger reports the mandatory fields', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n status: 'approved',\n };\n const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');\n sinon\n .stub(OrderRepository.prototype, 'addOrder')\n .rejects(new AppError('saving-failed', 'Order could not be saved', 500));\n const loggerDouble = sinon.stub(logger, 'error');\n\n //Act\n await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n expect(loggerDouble).toHaveBeenCalledWith({\n name: 'saving-failed',\n status: 500,\n stack: expect.any(String),\n message: expect.any(String),\n });\n expect(\n metricsExporterDouble).toHaveBeenCalledWith('error', {\n errorName: 'example-error',\n })\n});\n")),(0,s.kt)("h2",{id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code"},"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, ",(0,s.kt)("strong",{parentName:"p"},"hopefully if your code subscribed"),". How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:"),(0,s.kt)("p",null,"researches says that, rejection"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {\n //Arrange\n const loggerDouble = sinon.stub(logger, 'error');\n const processExitListener = sinon.stub(process, 'exit');\n const errorToThrow = new Error('An error that wont be caught \ud83d\ude33');\n\n //Act\n process.emit('uncaughtException', errorToThrow); //\ud83d\udc48 Where the magic is\n\n // Assert\n expect(processExitListener.called).toBe(false);\n expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);\n});\n")),(0,s.kt)("h2",{id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all"},"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n mode: 'draft',\n externalIdentifier: uuid(), //no existing record has this value\n };\n\n //Act\n const { status: addingHTTPStatus } = await axiosAPIClient.post(\n '/order',\n orderToAdd\n );\n\n //Assert\n const { status: fetchingHTTPStatus } = await axiosAPIClient.get(\n `/order/externalIdentifier/${orderToAdd.externalIdentifier}`\n ); // Trying to get the order that should have failed\n expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({\n addingHTTPStatus: 400,\n fetchingHTTPStatus: 404,\n });\n // \ud83d\udc46 Check that no such record exists\n});\n")),(0,s.kt)("h2",{id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much"},"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When deleting an existing order, Then it should NOT be retrievable', async () => {\n // Arrange\n const orderToDelete = {\n userId: 1,\n productId: 2,\n };\n const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data\n .id; // We will delete this soon\n const orderNotToBeDeleted = orderToDelete;\n const notDeletedOrder = (\n await axiosAPIClient.post('/order', orderNotToBeDeleted)\n ).data.id; // We will not delete this\n\n // Act\n await axiosAPIClient.delete(`/order/${deletedOrder}`);\n\n // Assert\n const { status: getDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${deletedOrder}`\n );\n const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${notDeletedOrder}`\n );\n expect(getNotDeletedOrderStatus).toBe(200);\n expect(getDeletedOrderStatus).toBe(404);\n});\n")),(0,s.kt)("h2",{id:"-the-slow-collaborator-test---when-the-other-http-service-times-out"},"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock")," or ",(0,s.kt)("a",{parentName:"p",href:"https://wiremock.org/"},"wiremock"),". These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available ",(0,s.kt)("strong",{parentName:"p"},"in production"),", what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use ",(0,s.kt)("a",{parentName:"p",href:"https://sinonjs.org/releases/latest/fake-timers/"},"fake timers")," and trick the system into believing as few seconds passed in a single tick. If you're using ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock"),", it offers an interesting feature to simulate timeouts ",(0,s.kt)("strong",{parentName:"p"},"quickly"),": the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// In this example, our code accepts new Orders and while processing them approaches the Users Microservice\ntest('When users service times out, then return 503 (option 1 with fake timers)', async () => {\n //Arrange\n const clock = sinon.useFakeTimers();\n config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls\n nock(`${config.userServiceURL}/user/`)\n .get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout \ud83d\udc46\n .reply(200);\n const loggerDouble = sinon.stub(logger, 'error');\n const orderToAdd = {\n userId: 1,\n productId: 2,\n mode: 'approved',\n };\n\n //Act\n // \ud83d\udc47try to add new order which should fail due to User service not available\n const response = await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n // \ud83d\udc47At least our code does its best given this situation\n expect(response.status).toBe(503);\n expect(loggerDouble.lastCall.firstArg).toMatchObject({\n name: 'user-service-not-available',\n stack: expect.any(String),\n message: expect.any(String),\n });\n});\n")),(0,s.kt)("h2",{id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation"},"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why"),(0,s.kt)("p",null,"When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. ",(0,s.kt)("a",{parentName:"p",href:"https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-using-purge-queue.html"},"SQS demand 60 seconds")," to purge queues), to name a few challenges that you won't find when dealing with real DB"),(0,s.kt)("p",null,"Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/m-radzikowski/aws-sdk-client-mock"},"this one for SQS")," and you can code one ",(0,s.kt)("strong",{parentName:"p"},"easily")," yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("ol",null,(0,s.kt)("li",{parentName:"ol"},"Create a fake message queue that does almost nothing but record calls, see full example here")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class FakeMessageQueueProvider extends EventEmitter {\n // Implement here\n\n publish(message) {}\n\n consume(queueName, callback) {}\n}\n")),(0,s.kt)("ol",{start:2},(0,s.kt)("li",{parentName:"ol"},"Make your message queue client accept real or fake provider")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n // Pass to it a fake or real message queue\n constructor(customMessageQueueProvider) {}\n\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // Simple implementation can be found here:\n // https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js\n}\n")),(0,s.kt)("ol",{start:3},(0,s.kt)("li",{parentName:"ol"},"Expose a convenient function that tells when certain calls where made")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // \ud83d\udc47\n waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise\n}\n")),(0,s.kt)("ol",{start:4},(0,s.kt)("li",{parentName:"ol"},"The test is now short, flat and expressive \ud83d\udc47")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');\nconst MessageQueueClient = require('./libs/message-queue-client');\nconst newOrderService = require('./domain/newOrderService');\n\ntest('When a poisoned message arrives, then it is being rejected back', async () => {\n // Arrange\n const messageWithInvalidSchema = { nonExistingProperty: 'invalid\u274c' };\n const messageQueueClient = new MessageQueueClient(\n new FakeMessageQueueProvider()\n );\n // Subscribe to new messages and passing the handler function\n messageQueueClient.consume('orders.new', newOrderService.addOrder);\n\n // Act\n await messageQueueClient.publish('orders.new', messageWithInvalidSchema);\n // Now all the layers of the app will get stretched \ud83d\udc46, including logic and message queue libraries\n\n // Assert\n await messageQueueClient.waitFor('reject', { howManyTimes: 1 });\n // \ud83d\udc46 This tells us that eventually our code asked the message queue client to reject this poisoned message\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/recipes/message-queue/fake-message-queue.test.js"},"is here")),(0,s.kt)("h2",{id:"-test-the-package-as-a-consumer"},"\ud83d\udce6 Test the package as a consumer"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts ",(0,s.kt)("em",{parentName:"p"},"that were built"),". See the mismatch here? ",(0,s.kt)("em",{parentName:"p"},"after")," running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,"Consider the following scenario, you're developing a library, and you wrote this code:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// index.js\nexport * from './calculate.js';\n\n// calculate.js \ud83d\udc48\nexport function calculate() {\n return 1;\n}\n")),(0,s.kt)("p",null,"Then some tests:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"import { calculate } from './index.js';\n\ntest('should return 1', () => {\n expect(calculate()).toBe(1);\n})\n\n\u2705 All tests pass \ud83c\udf8a\n")),(0,s.kt)("p",null,"Finally configure the package.json:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json5"},'{\n // ....\n "files": [\n "index.js"\n ]\n}\n')),(0,s.kt)("p",null,"See, 100% coverage, all tests pass locally and in the CI \u2705, it just won't work in production \ud83d\udc79. Why? because you forgot to include the ",(0,s.kt)("inlineCode",{parentName:"p"},"calculate.js")," in the package.json ",(0,s.kt)("inlineCode",{parentName:"p"},"files")," array \ud83d\udc46"),(0,s.kt)("p",null,"What can we do instead? we can test the library as ",(0,s.kt)("em",{parentName:"p"},"its end-users"),". How? publish the package to a local registry like ",(0,s.kt)("a",{parentName:"p",href:"https://verdaccio.org/"},"verdaccio"),", let the tests install and approach the ",(0,s.kt)("em",{parentName:"p"},"published")," code. Sounds troublesome? judge yourself \ud83d\udc47"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// global-setup.js\n\n// 1. Setup the in-memory NPM registry, one function that's it! \ud83d\udd25\nawait setupVerdaccio();\n\n// 2. Building our package \nawait exec('npm', ['run', 'build'], {\n cwd: packagePath,\n});\n\n// 3. Publish it to the in-memory registry\nawait exec('npm', ['publish', '--registry=http://localhost:4873'], {\n cwd: packagePath,\n});\n\n// 4. Installing it in the consumer directory\nawait exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {\n cwd: consumerPath,\n});\n\n// Test file in the consumerPath\n\n// 5. Test the package \ud83d\ude80\ntest(\"should succeed\", async () => {\n const { fn1 } = await import('my-package');\n\n expect(fn1()).toEqual(1);\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/rluvaton/e2e-verdaccio-example"},"is here")),(0,s.kt)("p",null,"What else this technique can be useful for?"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that"),(0,s.kt)("li",{parentName:"ul"},"You want to test ESM and CJS consumers"),(0,s.kt)("li",{parentName:"ul"},"If you have CLI application you can test it like your users"),(0,s.kt)("li",{parentName:"ul"},"Making sure all the voodoo magic in that babel file is working as expected")),(0,s.kt)("h2",{id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug"},"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -"),' Quite confidently I\'m sure that almost no team test their OpenAPI correctness. "It\'s just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.'),(0,s.kt)("p",null,"Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., ",(0,s.kt)("a",{parentName:"p",href:"https://pact.io"},"PACT"),"), there are also leaner approaches that gets you covered ",(0,s.kt)("em",{parentName:"p"},"easily and quickly")," (at the price of covering less risks)."),(0,s.kt)("p",null,"The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, an API throw a new error status")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"if (doesOrderCouponAlreadyExist) {\n throw new AppError('duplicated-coupon', { httpStatus: 409 });\n}\n")),(0,s.kt)("p",null,"The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json"},'"responses": {\n "200": {\n "description": "successful",\n }\n ,\n "400": {\n "description": "Invalid ID",\n "content": {}\n },// No 409 in this list\ud83d\ude32\ud83d\udc48\n}\n\n')),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const jestOpenAPI = require('jest-openapi');\njestOpenAPI('../openapi.json');\n\ntest('When an order with duplicated coupon is added , then 409 error should get returned', async () => {\n // Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n couponId: uuid(),\n };\n await axiosAPIClient.post('/order', orderToAdd);\n\n // Act\n // We're adding the same coupon twice \ud83d\udc47\n const receivedResponse = await axios.post('/order', orderToAdd);\n\n // Assert;\n expect(receivedResponse.status).toBe(409);\n expect(res).toSatisfyApiSpec();\n // This \ud83d\udc46 will throw if the API response, body or status, is different that was it stated in the OpenAPI\n});\n")),(0,s.kt)("p",null,"Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"beforeAll(() => {\n axios.interceptors.response.use((response) => {\n expect(response.toSatisfyApiSpec());\n // With this \ud83d\udc46, add nothing to the tests - each will fail if the response deviates from the docs\n });\n});\n")),(0,s.kt)("h2",{id:"even-more-ideas"},"Even more ideas"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Test readiness and health routes"),(0,s.kt)("li",{parentName:"ul"},"Test message queue connection failures"),(0,s.kt)("li",{parentName:"ul"},"Test JWT and JWKS failures"),(0,s.kt)("li",{parentName:"ul"},"Test security-related things like CSRF tokens"),(0,s.kt)("li",{parentName:"ul"},"Test your HTTP client retry mechanism (very easy with nock)"),(0,s.kt)("li",{parentName:"ul"},"Test that the DB migration succeed and the new code can work with old records format"),(0,s.kt)("li",{parentName:"ul"},"Test DB connection disconnects")),(0,s.kt)("h2",{id:"its-not-just-ideas-it-a-whole-new-mindset"},"It's not just ideas, it a whole new mindset"),(0,s.kt)("p",null,"The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'"))}h.isMDXComponent=!0},8933:(e,t,n)=>{n.d(t,{Z:()=>a});const a=n.p+"assets/images/the-hidden-corners-44855c2e5d9184502e1dc72b07d53cef.png"}}]); \ No newline at end of file diff --git a/assets/js/a7023ddc.bdc2dd45.js b/assets/js/a7023ddc.bdc2dd45.js deleted file mode 100644 index 999c8e95..00000000 --- a/assets/js/a7023ddc.bdc2dd45.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[1713],{3457:t=>{t.exports=JSON.parse('[{"label":"node.js","permalink":"/blog/tags/node-js","count":5},{"label":"testing","permalink":"/blog/tags/testing","count":3},{"label":"component-test","permalink":"/blog/tags/component-test","count":1},{"label":"fastify","permalink":"/blog/tags/fastify","count":4},{"label":"unit-test","permalink":"/blog/tags/unit-test","count":1},{"label":"integration","permalink":"/blog/tags/integration","count":1},{"label":"nock","permalink":"/blog/tags/nock","count":1},{"label":"monorepo","permalink":"/blog/tags/monorepo","count":1},{"label":"decisions","permalink":"/blog/tags/decisions","count":1},{"label":"express","permalink":"/blog/tags/express","count":4},{"label":"practica","permalink":"/blog/tags/practica","count":3},{"label":"prisma","permalink":"/blog/tags/prisma","count":1},{"label":"nestjs","permalink":"/blog/tags/nestjs","count":2},{"label":"passport","permalink":"/blog/tags/passport","count":2},{"label":"dotenv","permalink":"/blog/tags/dotenv","count":2},{"label":"supertest","permalink":"/blog/tags/supertest","count":2}]')}}]); \ No newline at end of file diff --git a/assets/js/a7023ddc.c09b8104.js b/assets/js/a7023ddc.c09b8104.js new file mode 100644 index 00000000..f39e408a --- /dev/null +++ b/assets/js/a7023ddc.c09b8104.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[1713],{3457:t=>{t.exports=JSON.parse('[{"label":"monorepo","permalink":"/blog/tags/monorepo","count":1},{"label":"decisions","permalink":"/blog/tags/decisions","count":1},{"label":"node.js","permalink":"/blog/tags/node-js","count":5},{"label":"testing","permalink":"/blog/tags/testing","count":3},{"label":"component-test","permalink":"/blog/tags/component-test","count":1},{"label":"fastify","permalink":"/blog/tags/fastify","count":4},{"label":"unit-test","permalink":"/blog/tags/unit-test","count":1},{"label":"integration","permalink":"/blog/tags/integration","count":1},{"label":"nock","permalink":"/blog/tags/nock","count":1},{"label":"express","permalink":"/blog/tags/express","count":4},{"label":"practica","permalink":"/blog/tags/practica","count":3},{"label":"prisma","permalink":"/blog/tags/prisma","count":1},{"label":"nestjs","permalink":"/blog/tags/nestjs","count":2},{"label":"passport","permalink":"/blog/tags/passport","count":2},{"label":"dotenv","permalink":"/blog/tags/dotenv","count":2},{"label":"supertest","permalink":"/blog/tags/supertest","count":2}]')}}]); \ No newline at end of file diff --git a/assets/js/b2f554cd.07401b26.js b/assets/js/b2f554cd.07401b26.js new file mode 100644 index 00000000..9f8d88d8 --- /dev/null +++ b/assets/js/b2f554cd.07401b26.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[1477],{10:e=>{e.exports=JSON.parse('{"blogPosts":[{"id":"monorepo-backend","metadata":{"permalink":"/blog/monorepo-backend","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/which-monorepo/index.md","source":"@site/blog/which-monorepo/index.md","title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","description":"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we\'d like to share our considerations in choosing our monorepo tooling","date":"2023-07-11T13:19:18.000Z","formattedDate":"July 11, 2023","tags":[{"label":"monorepo","permalink":"/blog/tags/monorepo"},{"label":"decisions","permalink":"/blog/tags/decisions"}],"readingTime":16.925,"hasTruncateMarker":true,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"},{"name":"Michael Salomon","title":"Practica.js core maintainer","url":"https://github.com/mikicho","imageURL":"https://avatars.githubusercontent.com/u/11459632?v=4","key":"michaelsalomon"}],"frontMatter":{"slug":"monorepo-backend","title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","authors":["goldbergyoni","michaelsalomon"],"tags":["monorepo","decisions"]},"nextItem":{"title":"Testing the dark scenarios of your Node.js application","permalink":"/blog/testing-the-dark-scenarios-of-your-nodejs-application"}},"content":"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in [Practica.js](https://github.com/practicajs/practica). In this post, we\'d like to share our considerations in choosing our monorepo tooling\\n\\n![Monorepos](./monorepo-high-level.png)\\n\\n## What are we looking\xa0at\\n\\n\\nThe Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries \u2014 [Lerna- has just retired.](https://github.com/lerna/lerna/issues/2703) When looking closely, it might not be just a coincidence \u2014 With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused \u2014 What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you\u2019re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow.\\n\\nThis post is concerned with backend-only and Node.js. It also scoped to _typical_ business solutions. If you\u2019re Google/FB developer who is faced with 8,000 packages \u2014 sorry, you need special gear. Consequently, monster Monorepo tooling like [Bazel](https://github.com/thundergolfer/example-bazel-monorepo) is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it\u2019s not actually maintained anymore \u2014 it\u2019s a good baseline for comparison).\\n\\nLet\u2019s start? When human beings use the term Monorepo, they typically refer to one or more of the following _4 layers below._ Each one of them can bring value to your project, each has different consequences, tooling, and features:\\n\\n\x3c!--truncate--\x3e\\n\\n\\n# Layer 1: Plain old folders to stay on top of your code\\n\\nWith zero tooling and only by having all the Microservice and libraries together in the same root folder, a developer gets great management perks and tons of value: Navigation, search across components, deleting a library instantly, debugging, _quickly_ adding new components. Consider the alternative with multi-repo approach \u2014 adding a new component for modularity demands opening and configuring a new GitHub repository. Not just a hassle but also greater chances of developers choosing the short path and including the new code in some semi-relevant existing package. In plain words, zero-tooling Monorepos can increase modularity.\\n\\nThis layer is often overlooked. If your codebase is not huge and the components are highly decoupled (more on this later)\u2014 it might be all you need. We\u2019ve seen a handful of successful Monorepo solutions without any special tooling.\\n\\nWith that said, some of the newer tools augment this experience with interesting features:\\n\\n- Both [Turborepo](https://turborepo.org/) and [Nx](https://nx.dev/structure/dependency-graph) and also [Lerna](https://www.npmjs.com/package/lerna-dependency-graph) provide a visual representation of the packages\u2019 dependencies\\n- [Nx allows \u2018visibility rules\u2019](https://nx.dev/structure/monorepo-tags) which is about enforcing who can use what. Consider, a \u2018checkout\u2019 library that should be approached only by the \u2018order Microservice\u2019 \u2014 deviating from this will result in failure during development (not runtime enforcement)\\n\\n![](https://miro.medium.com/max/1400/0*pHZKRlGT6iOKCmzg.jpg)\\n\\nNx dependencies graph\\n\\n- [Nx workspace generator](https://nx.dev/generators/workspace-generators) allows scaffolding out components. Whenever a team member needs to craft a new controller/library/class/Microservice, she just invokes a CLI command which products code based on a community or organization template. This enforces consistency and best practices sharing\\n\\n# Layer 2: Tasks and pipeline to build your code efficiently\\n\\nEven in a world of autonomous components, there are management tasks that must be applied in a batch like applying a security patch via npm update, running the tests of _multiple_ components that were affected by a change, publish 3 related libraries to name a few examples. All Monorepo tools support this basic functionality of invoking some command over a group of packages. For example, Lerna, Nx, and Turborepo do.\\n\\n![](https://miro.medium.com/max/1400/1*wu7xtN97-Ihz4uCSDwd0mA.png)\\n\\nApply some commands over multiple packages\\n\\nIn some projects, invoking a cascading command is all you need. Mostly if each package has an autonomous life cycle and the build process spans a single package (more on this later). In some other types of projects where the workflow demands testing/running and publishing/deploying many packages together \u2014 this will end in a terribly slow experience. Consider a solution with hundred of packages that are transpiled and bundled \u2014 one might wait minutes for a wide test to run. While it\u2019s not always a great practice to rely on wide/E2E tests, it\u2019s quite common in the wild. This is exactly where the new wave of Monorepo tooling shines \u2014 _deeply_ optimizing the build process. I should say this out loud: These tools bring beautiful and innovative build optimizations:\\n\\n- **Parallelization \u2014** If two commands or packages are orthogonal to each other, the commands will run in two different threads or processes. Typically your quality control involves testing, lining, license checking, CVE checking \u2014 why not parallelize?\\n- **Smart execution plan \u2014**Beyond parallelization, the optimized tasks execution order is determined based on many factors. Consider a build that includes A, B, C where A, C depend on B \u2014 naively, a build system would wait for B to build and only then run A & C. This can be optimized if we run A & C\u2019s _isolated_ unit tests _while_ building B and not afterward. By running task in parallel as early as possible, the overall execution time is improved \u2014 this has a remarkable impact mostly when hosting a high number of components. See below a visualization example of a pipeline improvement\\n\\n![](https://miro.medium.com/max/1400/0*C6cxCblQU8ckTIQk.png)\\n\\nA modern tool advantage over old Lerna. Taken from Turborepo website\\n\\n- **Detect who is affected by a change \u2014** Even on a system with high coupling between packages, it\u2019s usually not necessary to run _all_ packages rather than only those who are affected by a change. What exactly is \u2018affected\u2019? Packages/Microservices that depend upon another package that has changed. Some of the toolings can ignore minor changes that are unlikely to break others. This is not a great performance booster but also an amazing testing feature \u2014developers can get quick feedback on whether any of their clients were broken. Both Nx and Turborepo support this feature. Lerna can tell only which of the Monorepo package has changed\\n- **Sub-systems (i.e., projects) \u2014** Similarly to \u2018affected\u2019 above, modern tooling can realize portions of the graph that are inter-connected (a project or application) while others are not reachable by the component in context (another project) so they know to involve only packages of the relevant group\\n- **Caching \u2014** This is a serious speed booster: Nx and Turborepo cache the result/output of tasks and avoid running them again on consequent builds if unnecessary. For example, consider long-running tests of a Microservice, when commanding to re-build this Microservice, the tooling might realize that nothing has changed and the test will get skipped. This is achieved by generating a hashmap of all the dependent resources \u2014 if any of these resources haven\u2019t change, then the hashmap will be the same and the task will get skipped. They even cache the stdout of the command, so when you run a cached version it acts like the real thing \u2014 consider running 200 tests, seeing all the log statements of the tests, getting results over the terminal in 200 ms, everything acts like \u2018real testing while in fact, the tests did not run at all rather the cache!\\n- **Remote caching \u2014** Similarly to caching, only by placing the task\u2019s hashmaps and result on a global server so further executions on other team member\u2019s computers will also skip unnecessary tasks. In huge Monorepo projects that rely on E2E tests and must build all packages for development, this can save a great deal of time\\n\\n# Layer 3: Hoist your dependencies to boost npm installation\\n\\nThe speed optimizations that were described above won\u2019t be of help if the bottleneck is the big bull of mud that is called \u2018npm install\u2019 (not to criticize, it\u2019s just hard by nature). Take a typical scenario as an example, given dozens of components that should be built, they could easily trigger the installation of thousands of sub-dependencies. Although they use quite similar dependencies (e.g., same logger, same ORM), if the dependency version is not equal then npm will duplicate ([the NPM doppelgangers problem](https://rushjs.io/pages/advanced/npm_doppelgangers/)) the installation of those packages which might result in a long process.\\n\\nThis is where the workspace line of tools (e.g., Yarn workspace, npm workspaces, PNPM) kicks in and introduces some optimization \u2014 Instead of installing dependencies inside each component \u2018NODE_MODULES\u2019 folder, it will create one centralized folder and link all the dependencies over there. This can show a tremendous boost in install time for huge projects. On the other hand, if you always focus on one component at a time, installing the packages of a single Microservice/library should not be a concern.\\n\\nBoth Nx and Turborepo can rely on the package manager/workspace to provide this layer of optimizations. In other words, Nx and Turborepo are the layer above the package manager who take care of optimized dependencies installation.\\n\\n![](https://miro.medium.com/max/1400/1*dhyCWSbzpIi5iagR4OB4zQ.png)\\n\\nOn top of this, Nx introduces one more non-standard, maybe even controversial, technique: There might be only ONE package.json at the root folder of the entire Monorepo. By default, when creating components using Nx, they will not have their own package.json! Instead, all will share the root package.json. Going this way, all the Microservice/libraries share their dependencies and the installation time is improved. Note: It\u2019s possible to create \u2018publishable\u2019 components that do have a package.json, it\u2019s just not the default.\\n\\nI\u2019m concerned here. Sharing dependencies among packages increases the coupling, what if Microservice1 wishes to bump dependency1 version but Microservice2 can\u2019t do this at the moment? Also, package.json is part of Node.js _runtime_ and excluding it from the component root loses important features like package.json main field or ESM exports (telling the clients which files are exposed). I ran some POC with Nx last week and found myself blocked \u2014 library B was wadded, I tried to import it from Library A but couldn\u2019t get the \u2018import\u2019 statement to specify the right package name. The natural action was to open B\u2019s package.json and check the name, but there is no Package.json\u2026 How do I determine its name? Nx docs are great, finally, I found the answer, but I had to spend time learning a new \u2018framework\u2019.\\n\\n# Stop for a second: It\u2019s all about your workflow\\n\\nWe deal with tooling and features, but it\u2019s actually meaningless evaluating these options before determining whether your preferred workflow is _synchronized or independent_ (we will discuss this in a few seconds)_._ This upfront _fundamental_ decision will change almost everything.\\n\\nConsider the following example with 3 components: Library 1 is introducing some major and breaking changes, Microservice1 and Microservice2 depend upon Library1 and should react to those breaking changes. How?\\n\\n**Option A \u2014 The synchronized workflow-** Going with this development style, all the three components will be developed and deployed in one chunk _together_. Practically, a developer will code the changes in Library1, test libray1 and also run wide integration/e2e tests that include Microservice1 and Microservice2. When they\'re ready, the version of all components will get bumped. Finally, they will get deployed _together._\\n\\nGoing with this approach, the developer has the chance of seeing the full flow from the client\'s perspective (Microservice1 and 2), the tests cover not only the library but also through the eyes of the clients who actually use it. On the flip side, it mandates updating all the depend-upon components (could be dozens), doing so increases the risk\u2019s blast radius as more units are affected and should be considered before deployment. Also, working on a large unit of work demands building and testing more things which will slow the build.\\n\\n**Option B \u2014 Independent workflow-** This style is about working a unit by unit, one bite at a time, and deploy each component independently based on its personal business considerations and priority. This is how it goes: A developer makes the changes in Library1, they must be tested carefully in the scope of Library1. Once she is ready, the SemVer is bumped to a new major and the library is published to a package manager registry (e.g., npm). What about the client Microservices? Well, the team of Microservice2 is super-busy now with other priorities, and skip this update for now (the same thing as we all delay many of our npm updates,). However, Microservice1 is very much interested in this change \u2014 The team has to pro-actively update this dependency and grab the latest changes, run the tests and when they are ready, today or next week \u2014 deploy it.\\n\\nGoing with the independent workflow, the library author can move much faster because she does not need to take into account 2 or 30 other components \u2014 some are coded by different teams. This workflow also _forces her_ to write efficient tests against the library \u2014 it\u2019s her only safety net and is likely to end with autonomous components that have low coupling to others. On the other hand, testing in isolation without the client\u2019s perspective loses some dimension of realism. Also, if a single developer has to update 5 units \u2014 publishing each individually to the registry and then updating within all the dependencies can be a little tedious.\\n\\n![](https://miro.medium.com/max/1400/1*eeJFL3_vo5tCrWvVY-surg.png)\\n\\nSynchronized and independent workflows illustrated\\n\\n**On the illusion of synchronicity**\\n\\nIn distributed systems, it\u2019s not feasible to achieve 100% synchronicity \u2014 believing otherwise can lead to design faults. Consider a breaking change in Microservice1, now its client Microservice2 is adapting and ready for the change. These two Microservices are deployed together but due to the nature of Microservices and distributed runtime (e.g., Kubernetes) the deployment of Microservice1 only fail. Now, Microservice2\u2019s code is not aligned with Microservice1 production and we are faced with a production bug. This line of failures can be handled to an extent also with a synchronized workflow \u2014 The deployment should orchestrate the rollout of each unit so each one is deployed at a time. Although this approach is doable, it increased the chances of large-scoped rollback and increases deployment fear.\\n\\nThis fundamental decision, synchronized or independent, will determine so many things \u2014 Whether performance is an issue or not at all (when working on a single unit), hoisting dependencies or leaving a dedicated node_modules in every package\u2019s folder, and whether to create a local link between packages which is described in the next paragraph.\\n\\n# Layer 4: Link your packages for immediate feedback\\n\\nWhen having a Monorepo, there is always the unavoidable dilemma of how to link between the components:\\n\\n**Option 1: Using npm \u2014** Each library is a standard npm package and its client installs it via the standards npm commands. Given Microservice1 and Library1, this will end with two copies of Library1: the one inside Microservices1/NODE_MODULES (i.e., the local copy of the consuming Microservice), and the 2nd is the development folder where the team is coding Library1.\\n\\n**Option2: Just a plain folder \u2014** With this, Library1 is nothing but a logical module inside a folder that Microservice1,2,3 just locally imports. NPM is not involved here, it\u2019s just code in a dedicated folder. This is for example how Nest.js modules are represented.\\n\\nWith option 1, teams benefit from all the great merits of a package manager \u2014 SemVer(!), tooling, standards, etc. However, should one update Library1, the changes won\u2019t get reflected in Microservice1 since it is grabbing its copy from the npm registry and the changes were not published yet. This is a fundamental pain with Monorepo and package managers \u2014 one can\u2019t just code over multiple packages and test/run the changes.\\n\\nWith option 2, teams lose all the benefits of a package manager: Every change is propagated immediately to all of the consumers.\\n\\nHow do we bring the good from both worlds (presumably)? Using linking. Lerna, Nx, the various package manager workspaces (Yarn, npm, etc) allow using npm libraries and at the same time link between the clients (e.g., Microservice1) and the library. Under the hood, they created a symbolic link. In development mode, changes are propagated immediately, in deployment time \u2014 the copy is grabbed from the registry.\\n\\n![](https://miro.medium.com/max/1400/1*9PkNrnbnibFdbvPieq-y9g.png)\\n\\nLinking packages in a Monorepo\\n\\nIf you\u2019re doing the synchronized workflow, you\u2019re all set. Only now any risky change that is introduced by Library3, must be handled NOW by the 10 Microservices that consume it.\\n\\nIf favoring the independent workflow, this is of course a big concern. Some may call this direct linking style a \u2018monolith monorepo\u2019, or maybe a \u2018monolitho\u2019. However, when not linking, it\u2019s harder to debug a small issue between the Microservice and the npm library. What I typically do is _temporarily link_ (with npm link) between the packages_,_ debug, code, then finally remove the link.\\n\\nNx is taking a slightly more disruptive approach \u2014 it is using [TypeScript paths](https://www.typescriptlang.org/tsconfig#paths) to bind between the components. When Microservice1 is importing Library1, to avoid the full local path, it creates a TypeScript mapping between the library name and the full path. But wait a minute, there is no TypeScript in production so how could it work? Well, in serving/bundling time it webpacks and stitches the components together. Not a very standard way of doing Node.js work.\\n\\n# Closing: What should you use?\\n\\nIt\u2019s all about your workflow and architecture \u2014 a huge unseen cross-road stands in front of the Monorepo tooling decision.\\n\\n**Scenario A \u2014** If your architecture dictates a _synchronized workflow_ where all packages are deployed together, or at least developed in collaboration \u2014 then there is a strong need for a rich tool to manage this coupling and boost the performance. In this case, Nx might be a great choice.\\n\\nFor example, if your Microservice must keep the same versioning, or if the team really small and the same people are updating all the components, or if your modularization is not based on package manager but rather on framework-own modules (e.g., Nest.js), if you\u2019re doing frontend where the components inherently are published together, or if your testing strategy relies on E2E mostly \u2014 for all of these cases and others, Nx is a tool that was built to enhance the experience of coding many _relatively_ coupled components together. It is a great a sugar coat over systems that are unavoidably big and linked.\\n\\nIf your system is not inherently big or meant to synchronize packages deployment, fancy Monorepo features might increase the coupling between components. The Monorepo pyramid above draws a line between basic features that provide value without coupling components while other layers come with an architectural price to consider. Sometimes climbing up toward the tip is worth the consequences, just make this decision consciously.\\n\\n![](https://miro.medium.com/max/1400/1*c2qYYpVGG667bkum-gB-5Q.png)\\n\\n**Scenario B\u2014** If you\u2019re into an _independent workflow_ where each package is developed, tested, and deployed (almost) independently \u2014 then inherently there is no need to fancy tools to orchestrate hundreds of packages. Most of the time there is just one package in focus. This calls for picking a leaner and simpler tool \u2014 Turborepo. By going this route, Monorepo is not something that affects your architecture, but rather a scoped tool for faster build execution. One specific tool that encourages an independent workflow is [Bilt](https://github.com/giltayar/bilt) by Gil Tayar, it\u2019s yet to gain enough popularity but it might rise soon and is a great source to learn more about this philosophy of work.\\n\\n**In any scenario, consider workspaces \u2014** If you face performance issues that are caused by package installation, then the various workspace tools Yarn/npm/PNPM, can greatly minimize this overhead with a low footprint. That said, if you\u2019re working in an autonomous workflow, smaller are the chances of facing such issues. Don\u2019t just use tools unless there is a pain.\\n\\nWe tried to show the beauty of each and where it shines. If we\u2019re allowed to end this article with an opinionated choice: We greatly believe in an independent and autonomous workflow where the occasional developer of a package can code and deploy fearlessly without messing with dozens of other foreign packages. For this reason, Turborepo will be our favorite tool for the next season. We promise to tell you how it goes.\\n\\n# Bonus: Comparison table\\n\\nSee below a detailed comparison table of the various tools and features:\\n\\n![](https://miro.medium.com/max/1400/1*iHX_IdPW8XXXiZTyjFo6bw.png)\\n\\nPreview only, the complete table can be [found here](https://github.com/practicajs/practica/blob/main/docs/docs/decisions/monorepo.md)"},{"id":"testing-the-dark-scenarios-of-your-nodejs-application","metadata":{"permalink":"/blog/testing-the-dark-scenarios-of-your-nodejs-application","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/crucial-tests/index.md","source":"@site/blog/crucial-tests/index.md","title":"Testing the dark scenarios of your Node.js application","description":"Where the dead-bodies are covered","date":"2023-07-07T11:00:00.000Z","formattedDate":"July 7, 2023","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"testing","permalink":"/blog/tags/testing"},{"label":"component-test","permalink":"/blog/tags/component-test"},{"label":"fastify","permalink":"/blog/tags/fastify"},{"label":"unit-test","permalink":"/blog/tags/unit-test"},{"label":"integration","permalink":"/blog/tags/integration"},{"label":"nock","permalink":"/blog/tags/nock"}],"readingTime":20.3,"hasTruncateMarker":false,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"},{"name":"Raz Luvaton","title":"Practica.js core maintainer","url":"https://github.com/rluvaton","imageURL":"https://avatars.githubusercontent.com/u/16746759?v=4","key":"razluvaton"}],"frontMatter":{"slug":"testing-the-dark-scenarios-of-your-nodejs-application","date":"2023-07-07T11:00","hide_table_of_contents":true,"title":"Testing the dark scenarios of your Node.js application","authors":["goldbergyoni","razluvaton"],"tags":["node.js","testing","component-test","fastify","unit-test","integration","nock"]},"prevItem":{"title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","permalink":"/blog/monorepo-backend"},"nextItem":{"title":"Practica v0.0.6 is alive","permalink":"/blog/practica-v0.0.6-is-alive"}},"content":"## Where the dead-bodies are covered\\n\\nThis post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked\\n\\nSome context first: How do we test a modern backend? With [the testing diamond](https://ritesh-kapoor.medium.com/testing-automation-what-are-pyramids-and-diamonds-67494fec7c55), of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we\'ve also written [a guide with 50 best practices for integration tests in Node.js](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n\\nBut there is a pitfall: most developers write _only_ semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don\'t simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime\\n\\n![The hidden corners](./the-hidden-corners.png)\\n\\nHere are a handful of examples that might open your mind to a whole new class of risks and tests\\n\\n_Side note: I\'ve just released a comprehensive testing course that I\'ve been working on for two years. \ud83c\udf81 It\'s now on sale, but only for the month of July. Check it out at [testjavascript.com](https://testjavascript.com/)_\\n\\n## **Test Examples**\\n\\n## \ud83e\udddf\u200d\u2640\ufe0f The zombie process test\\n\\n**\ud83d\udc49What & so what? -** In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see [readiness probe](https://komodor.com/learn/kubernetes-readiness-probes-a-practical-guide/#:~:text=A%20readiness%20probe%20allows%20Kubernetes,on%20deletion%20of%20a%20pod.)). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a \'zombie process\'. In this scenario, the runtime platform won\'t realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!\\n\\n**\ud83d\udcdd Code**\\n\\n**Code under test, api.js:**\\n\\n```javascript\\n// A common express server initialization\\nconst startWebServer = () => {\\n return new Promise((resolve, reject) => {\\n try {\\n // A typical Express setup\\n expressApp = express();\\n defineRoutes(expressApp); // a function that defines all routes\\n expressApp.listen(process.env.WEB_SERVER_PORT);\\n } catch (error) {\\n //log here, fire a metric, maybe even retry and finally:\\n process.exit();\\n }\\n });\\n};\\n```\\n\\n**The test:**\\n\\n```javascript\\nconst api = require(\'./entry-points/api\'); // our api starter that exposes \'startWebServer\' function\\nconst sinon = require(\'sinon\'); // a mocking library\\n\\ntest(\'When an error happens during the startup phase, then the process exits\', async () => {\\n // Arrange\\n const processExitListener = sinon.stub(process, \'exit\');\\n // \ud83d\udc47 Choose a function that is part of the initialization phase and make it fail\\n sinon\\n .stub(routes, \'defineRoutes\')\\n .throws(new Error(\'Cant initialize connection\'));\\n\\n // Act\\n await api.startWebServer();\\n\\n // Assert\\n expect(processExitListener.called).toBe(true);\\n});\\n```\\n \\n## \ud83d\udc40 The observability test\\n\\n**\ud83d\udc49What & why -** For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error **correctly observable**. In plain words, ensuring that it\'s being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, _including stack trace_, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn\'t care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\ntest(\'When exception is throw during request, Then logger reports the mandatory fields\', async () => {\\n //Arrange\\n const orderToAdd = {\\n userId: 1,\\n productId: 2,\\n status: \'approved\',\\n };\\n const metricsExporterDouble = sinon.stub(metricsExporter, \'fireMetric\');\\n sinon\\n .stub(OrderRepository.prototype, \'addOrder\')\\n .rejects(new AppError(\'saving-failed\', \'Order could not be saved\', 500));\\n const loggerDouble = sinon.stub(logger, \'error\');\\n\\n //Act\\n await axiosAPIClient.post(\'/order\', orderToAdd);\\n\\n //Assert\\n expect(loggerDouble).toHaveBeenCalledWith({\\n name: \'saving-failed\',\\n status: 500,\\n stack: expect.any(String),\\n message: expect.any(String),\\n });\\n expect(\\n metricsExporterDouble).toHaveBeenCalledWith(\'error\', {\\n errorName: \'example-error\',\\n })\\n});\\n```\\n\\n## \ud83d\udc7d The \'unexpected visitor\' test - when an uncaught exception meets our code\\n\\n**\ud83d\udc49What & why -** A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let\'s focus on the 2nd assumption: it\'s common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on(\'error\', ...). To name a few examples. These errors will find their way to the global process.on(\'uncaughtException\') handler, **hopefully if your code subscribed**. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here\'s a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is \'borderless\', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here\'s an example:\\n\\nresearches says that, rejection\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\ntest(\'When an unhandled exception is thrown, then process stays alive and the error is logged\', async () => {\\n //Arrange\\n const loggerDouble = sinon.stub(logger, \'error\');\\n const processExitListener = sinon.stub(process, \'exit\');\\n const errorToThrow = new Error(\'An error that wont be caught \ud83d\ude33\');\\n\\n //Act\\n process.emit(\'uncaughtException\', errorToThrow); //\ud83d\udc48 Where the magic is\\n\\n // Assert\\n expect(processExitListener.called).toBe(false);\\n expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);\\n});\\n```\\n\\n## \ud83d\udd75\ud83c\udffc The \'hidden effect\' test - when the code should not mutate at all\\n\\n**\ud83d\udc49What & so what -** In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn\'t have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn\'t guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you\'re not cleaning the DB often (like me, but that\'s another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\nit(\'When adding an invalid order, then it returns 400 and NOT retrievable\', async () => {\\n //Arrange\\n const orderToAdd = {\\n userId: 1,\\n mode: \'draft\',\\n externalIdentifier: uuid(), //no existing record has this value\\n };\\n\\n //Act\\n const { status: addingHTTPStatus } = await axiosAPIClient.post(\\n \'/order\',\\n orderToAdd\\n );\\n\\n //Assert\\n const { status: fetchingHTTPStatus } = await axiosAPIClient.get(\\n `/order/externalIdentifier/${orderToAdd.externalIdentifier}`\\n ); // Trying to get the order that should have failed\\n expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({\\n addingHTTPStatus: 400,\\n fetchingHTTPStatus: 404,\\n });\\n // \ud83d\udc46 Check that no such record exists\\n});\\n```\\n\\n## \ud83e\udde8 The \'overdoing\' test - when the code should mutate but it\'s doing too much\\n\\n**\ud83d\udc49What & why -** This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here\'s a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more \'control\' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\ntest(\'When deleting an existing order, Then it should NOT be retrievable\', async () => {\\n // Arrange\\n const orderToDelete = {\\n userId: 1,\\n productId: 2,\\n };\\n const deletedOrder = (await axiosAPIClient.post(\'/order\', orderToDelete)).data\\n .id; // We will delete this soon\\n const orderNotToBeDeleted = orderToDelete;\\n const notDeletedOrder = (\\n await axiosAPIClient.post(\'/order\', orderNotToBeDeleted)\\n ).data.id; // We will not delete this\\n\\n // Act\\n await axiosAPIClient.delete(`/order/${deletedOrder}`);\\n\\n // Assert\\n const { status: getDeletedOrderStatus } = await axiosAPIClient.get(\\n `/order/${deletedOrder}`\\n );\\n const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(\\n `/order/${notDeletedOrder}`\\n );\\n expect(getNotDeletedOrderStatus).toBe(200);\\n expect(getDeletedOrderStatus).toBe(404);\\n});\\n```\\n\\n## \ud83d\udd70 The \'slow collaborator\' test - when the other HTTP service times out\\n\\n**\ud83d\udc49What & why -** When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it\'s harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like [nock](https://github.com/nock/nock) or [wiremock](https://wiremock.org/). These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available **in production**, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can\'t wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other \'chaotic\' scenarios. Question left is how to simulate slow response without having slow tests? You may use [fake timers](https://sinonjs.org/releases/latest/fake-timers/) and trick the system into believing as few seconds passed in a single tick. If you\'re using [nock](https://github.com/nock/nock), it offers an interesting feature to simulate timeouts **quickly**: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\n// In this example, our code accepts new Orders and while processing them approaches the Users Microservice\\ntest(\'When users service times out, then return 503 (option 1 with fake timers)\', async () => {\\n //Arrange\\n const clock = sinon.useFakeTimers();\\n config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls\\n nock(`${config.userServiceURL}/user/`)\\n .get(\'/1\', () => clock.tick(2000)) // Reply delay is bigger than configured timeout \ud83d\udc46\\n .reply(200);\\n const loggerDouble = sinon.stub(logger, \'error\');\\n const orderToAdd = {\\n userId: 1,\\n productId: 2,\\n mode: \'approved\',\\n };\\n\\n //Act\\n // \ud83d\udc47try to add new order which should fail due to User service not available\\n const response = await axiosAPIClient.post(\'/order\', orderToAdd);\\n\\n //Assert\\n // \ud83d\udc47At least our code does its best given this situation\\n expect(response.status).toBe(503);\\n expect(loggerDouble.lastCall.firstArg).toMatchObject({\\n name: \'user-service-not-available\',\\n stack: expect.any(String),\\n message: expect.any(String),\\n });\\n});\\n```\\n\\n## \ud83d\udc8a The \'poisoned message\' test - when the message consumer gets an invalid payload that might put it in stagnation\\n\\n**\ud83d\udc49What & so what -** When testing flows that start or end in a queue, I bet you\'re going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you\'re using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the \'poisoned message\'. To mitigate this risk, the tests\' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why\\n\\nWhen testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. [SQS demand 60 seconds](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-using-purge-queue.html) to purge queues), to name a few challenges that you won\'t find when dealing with real DB\\n\\nHere is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By \'fake\' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like [this one for SQS](https://github.com/m-radzikowski/aws-sdk-client-mock) and you can code one **easily** yourself. No worries, I\'m not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):\\n\\n**\ud83d\udcdd Code**\\n\\n1. Create a fake message queue that does almost nothing but record calls, see full example here\\n\\n```javascript\\nclass FakeMessageQueueProvider extends EventEmitter {\\n // Implement here\\n\\n publish(message) {}\\n\\n consume(queueName, callback) {}\\n}\\n```\\n\\n2. Make your message queue client accept real or fake provider\\n\\n```javascript\\nclass MessageQueueClient extends EventEmitter {\\n // Pass to it a fake or real message queue\\n constructor(customMessageQueueProvider) {}\\n\\n publish(message) {}\\n\\n consume(queueName, callback) {}\\n\\n // Simple implementation can be found here:\\n // https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js\\n}\\n```\\n\\n3. Expose a convenient function that tells when certain calls where made\\n\\n```javascript\\nclass MessageQueueClient extends EventEmitter {\\n publish(message) {}\\n\\n consume(queueName, callback) {}\\n\\n // \ud83d\udc47\\n waitForEvent(eventName: \'publish\' | \'consume\' | \'acknowledge\' | \'reject\', howManyTimes: number) : Promise\\n}\\n```\\n\\n4. The test is now short, flat and expressive \ud83d\udc47\\n\\n```javascript\\nconst FakeMessageQueueProvider = require(\'./libs/fake-message-queue-provider\');\\nconst MessageQueueClient = require(\'./libs/message-queue-client\');\\nconst newOrderService = require(\'./domain/newOrderService\');\\n\\ntest(\'When a poisoned message arrives, then it is being rejected back\', async () => {\\n // Arrange\\n const messageWithInvalidSchema = { nonExistingProperty: \'invalid\u274c\' };\\n const messageQueueClient = new MessageQueueClient(\\n new FakeMessageQueueProvider()\\n );\\n // Subscribe to new messages and passing the handler function\\n messageQueueClient.consume(\'orders.new\', newOrderService.addOrder);\\n\\n // Act\\n await messageQueueClient.publish(\'orders.new\', messageWithInvalidSchema);\\n // Now all the layers of the app will get stretched \ud83d\udc46, including logic and message queue libraries\\n\\n // Assert\\n await messageQueueClient.waitFor(\'reject\', { howManyTimes: 1 });\\n // \ud83d\udc46 This tells us that eventually our code asked the message queue client to reject this poisoned message\\n});\\n```\\n\\n**\ud83d\udcddFull code example -** [is here](https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/recipes/message-queue/fake-message-queue.test.js)\\n\\n## \ud83d\udce6 Test the package as a consumer\\n\\n**\ud83d\udc49What & why -** When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user\'s computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts _that were built_. See the mismatch here? _after_ running the tests, the package files are transpiled (I\'m looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files\\n\\n**\ud83d\udcdd Code**\\n\\nConsider the following scenario, you\'re developing a library, and you wrote this code:\\n```js\\n// index.js\\nexport * from \'./calculate.js\';\\n\\n// calculate.js \ud83d\udc48\\nexport function calculate() {\\n return 1;\\n}\\n```\\n\\nThen some tests:\\n```js\\nimport { calculate } from \'./index.js\';\\n\\ntest(\'should return 1\', () => {\\n expect(calculate()).toBe(1);\\n})\\n\\n\u2705 All tests pass \ud83c\udf8a\\n```\\n\\nFinally configure the package.json:\\n```json5\\n{\\n // ....\\n \\"files\\": [\\n \\"index.js\\"\\n ]\\n}\\n```\\n\\nSee, 100% coverage, all tests pass locally and in the CI \u2705, it just won\'t work in production \ud83d\udc79. Why? because you forgot to include the `calculate.js` in the package.json `files` array \ud83d\udc46\\n\\n\\nWhat can we do instead? we can test the library as _its end-users_. How? publish the package to a local registry like [verdaccio](https://verdaccio.org/), let the tests install and approach the *published* code. Sounds troublesome? judge yourself \ud83d\udc47\\n\\n**\ud83d\udcdd Code**\\n\\n```js\\n// global-setup.js\\n\\n// 1. Setup the in-memory NPM registry, one function that\'s it! \ud83d\udd25\\nawait setupVerdaccio();\\n\\n// 2. Building our package \\nawait exec(\'npm\', [\'run\', \'build\'], {\\n cwd: packagePath,\\n});\\n\\n// 3. Publish it to the in-memory registry\\nawait exec(\'npm\', [\'publish\', \'--registry=http://localhost:4873\'], {\\n cwd: packagePath,\\n});\\n\\n// 4. Installing it in the consumer directory\\nawait exec(\'npm\', [\'install\', \'my-package\', \'--registry=http://localhost:4873\'], {\\n cwd: consumerPath,\\n});\\n\\n// Test file in the consumerPath\\n\\n// 5. Test the package \ud83d\ude80\\ntest(\\"should succeed\\", async () => {\\n const { fn1 } = await import(\'my-package\');\\n\\n expect(fn1()).toEqual(1);\\n});\\n```\\n\\n**\ud83d\udcddFull code example -** [is here](https://github.com/rluvaton/e2e-verdaccio-example)\\n\\nWhat else this technique can be useful for?\\n\\n- Testing different version of peer dependency you support - let\'s say your package support react 16 to 18, you can now test that\\n- You want to test ESM and CJS consumers\\n- If you have CLI application you can test it like your users\\n- Making sure all the voodoo magic in that babel file is working as expected\\n\\n## \ud83d\uddde The \'broken contract\' test - when the code is great but its corresponding OpenAPI docs leads to a production bug\\n\\n**\ud83d\udc49What & so what -** Quite confidently I\'m sure that almost no team test their OpenAPI correctness. \\"It\'s just documentation\\", \\"we generate it automatically based on code\\" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.\\n\\nConsider the following scenario, you\'re requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don\'t forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the \'contract\' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., [PACT](https://pact.io)), there are also leaner approaches that gets you covered _easily and quickly_ (at the price of covering less risks).\\n\\nThe following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It\'s a pity that these libs can\'t assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:\\n\\n**\ud83d\udcdd Code**\\n\\n**Code under test, an API throw a new error status**\\n\\n```javascript\\nif (doesOrderCouponAlreadyExist) {\\n throw new AppError(\'duplicated-coupon\', { httpStatus: 409 });\\n}\\n```\\n\\nThe OpenAPI doesn\'t document HTTP status \'409\', no framework knows to update the OpenAPI doc based on thrown exceptions\\n\\n```json\\n\\"responses\\": {\\n \\"200\\": {\\n \\"description\\": \\"successful\\",\\n }\\n ,\\n \\"400\\": {\\n \\"description\\": \\"Invalid ID\\",\\n \\"content\\": {}\\n },// No 409 in this list\ud83d\ude32\ud83d\udc48\\n}\\n\\n```\\n\\n**The test code**\\n\\n```javascript\\nconst jestOpenAPI = require(\'jest-openapi\');\\njestOpenAPI(\'../openapi.json\');\\n\\ntest(\'When an order with duplicated coupon is added , then 409 error should get returned\', async () => {\\n // Arrange\\n const orderToAdd = {\\n userId: 1,\\n productId: 2,\\n couponId: uuid(),\\n };\\n await axiosAPIClient.post(\'/order\', orderToAdd);\\n\\n // Act\\n // We\'re adding the same coupon twice \ud83d\udc47\\n const receivedResponse = await axios.post(\'/order\', orderToAdd);\\n\\n // Assert;\\n expect(receivedResponse.status).toBe(409);\\n expect(res).toSatisfyApiSpec();\\n // This \ud83d\udc46 will throw if the API response, body or status, is different that was it stated in the OpenAPI\\n});\\n```\\n\\nTrick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in \'beforeAll\'. This covers all the tests against OpenAPI mismatches\\n\\n```javascript\\nbeforeAll(() => {\\n axios.interceptors.response.use((response) => {\\n expect(response.toSatisfyApiSpec());\\n // With this \ud83d\udc46, add nothing to the tests - each will fail if the response deviates from the docs\\n });\\n});\\n```\\n\\n## Even more ideas\\n\\n- Test readiness and health routes\\n- Test message queue connection failures\\n- Test JWT and JWKS failures\\n- Test security-related things like CSRF tokens\\n- Test your HTTP client retry mechanism (very easy with nock)\\n- Test that the DB migration succeed and the new code can work with old records format\\n- Test DB connection disconnects\\n- You may find many more examples at my fresh new testing course - [testjavascript.com](https://testjavascript.com)\\n\\n \\n## It\'s not just ideas, it a whole new mindset\\n\\nThe examples above were not meant only to be a checklist of \'don\'t forget\' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this \'production-oriented development\'\\n\\n**My new online testing course -** If you\'re intrigued with beyond the basics testing patterns, ]consider my online course which was just launched and is \ud83c\udf81 on sale for 30 days (July 2023)](https://testjavascript.com)"},{"id":"practica-v0.0.6-is-alive","metadata":{"permalink":"/blog/practica-v0.0.6-is-alive","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/v0.6-is-alive/index.md","source":"@site/blog/v0.6-is-alive/index.md","title":"Practica v0.0.6 is alive","description":"Where is our focus now?","date":"2022-12-10T10:00:00.000Z","formattedDate":"December 10, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"practica","permalink":"/blog/tags/practica"},{"label":"prisma","permalink":"/blog/tags/prisma"}],"readingTime":1.47,"hasTruncateMarker":false,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"},{"name":"Raz Luvaton","title":"Practica.js core maintainer","url":"https://github.com/rluvaton","imageURL":"https://avatars.githubusercontent.com/u/16746759?v=4","key":"razluvaton"},{"name":"Daniel Gluskin","title":"Practica.js core maintainer","url":"https://github.com/DanielGluskin","imageURL":"https://avatars.githubusercontent.com/u/17989958?v=4","key":"danielgluskin"},{"name":"Michael Salomon","title":"Practica.js core maintainer","url":"https://github.com/mikicho","imageURL":"https://avatars.githubusercontent.com/u/11459632?v=4","key":"michaelsalomon"}],"frontMatter":{"slug":"practica-v0.0.6-is-alive","date":"2022-12-10T10:00","hide_table_of_contents":true,"title":"Practica v0.0.6 is alive","authors":["goldbergyoni","razluvaton","danielgluskin","michaelsalomon"],"tags":["node.js","express","practica","prisma"]},"prevItem":{"title":"Testing the dark scenarios of your Node.js application","permalink":"/blog/testing-the-dark-scenarios-of-your-nodejs-application"},"nextItem":{"title":"Is Prisma better than your \'traditional\' ORM?","permalink":"/blog/is-prisma-better-than-your-traditional-orm"}},"content":"## Where is our focus now?\\n\\nWe work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback\\n\\n## What\'s new?\\n\\n### Request-level store\\n\\nEvery request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is \'request-id\' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in [AsyncLocal](https://nodejs.org/api/async_context.html) for this task\\n\\n### Hardened .dockerfile\\n\\nAlthough a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from [this article](https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/) and already apply 90% of the guidelines\\n\\n### Additional ORM option: Prisma\\n\\nPrisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma\\n\\nWhy did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this [blog post](https://practica.dev/blog/is-prisma-better-than-your-traditional-orm/)\\n\\n### Many small enhancements\\n\\nMore than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more\\n\\n## Where do I start?\\n\\nDefinitely follow the [getting started guide first](https://practica.dev/the-basics/getting-started-quickly) and then read the guide [coding with practica](https://practica.dev/the-basics/coding-with-practica) to realize its full power and genuine value. We will be thankful to receive your feedback"},{"id":"is-prisma-better-than-your-traditional-orm","metadata":{"permalink":"/blog/is-prisma-better-than-your-traditional-orm","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/is-prisma-better/index.md","source":"@site/blog/is-prisma-better/index.md","title":"Is Prisma better than your \'traditional\' ORM?","description":"Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?","date":"2022-12-07T11:00:00.000Z","formattedDate":"December 7, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"nestjs","permalink":"/blog/tags/nestjs"},{"label":"fastify","permalink":"/blog/tags/fastify"},{"label":"passport","permalink":"/blog/tags/passport"},{"label":"dotenv","permalink":"/blog/tags/dotenv"},{"label":"supertest","permalink":"/blog/tags/supertest"},{"label":"practica","permalink":"/blog/tags/practica"},{"label":"testing","permalink":"/blog/tags/testing"}],"readingTime":23.875,"hasTruncateMarker":true,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"}],"frontMatter":{"slug":"is-prisma-better-than-your-traditional-orm","date":"2022-12-07T11:00","hide_table_of_contents":true,"title":"Is Prisma better than your \'traditional\' ORM?","authors":["goldbergyoni"],"tags":["node.js","express","nestjs","fastify","passport","dotenv","supertest","practica","testing"]},"prevItem":{"title":"Practica v0.0.6 is alive","permalink":"/blog/practica-v0.0.6-is-alive"},"nextItem":{"title":"Popular Node.js patterns and tools to re-consider","permalink":"/blog/popular-nodejs-pattern-and-tools-to-reconsider"}},"content":"## Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?\\n\\n*Betteridge\'s law of headlines suggests that a \'headline that ends in a question mark can be answered by the word NO\'. Will this article follow this rule?*\\n\\nImagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it\'s hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained\\n\\n![Suite with stain](./suite.png)\\n\\n Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, \\"I wish we had something like (Java) hibernate or (.NET) Entity Framework\\" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don\'t feel delightful, some may say even mediocre. At least so I believed *before* writing this article...\\n\\nFrom time to time, a shiny new ORM is launched, and there is hope. Then soon it\'s realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It\'s gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the \'Ferrari\' ORM we\'ve been waiting for? Is it a game changer? If you\'re are the \'no ORM for me\' type, will this one make you convert your religion?\\n\\nIn [Practica.js](https://github.com/practicajs/practica) (the Node.js starter based off [Node.js best practices with 83,000 stars](https://github.com/goldbergyoni/nodebestpractices)) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?\\n\\nThis article is certainly not an \'ORM 101\' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It\'s compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren\'t covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs\\n\\nReady to explore how good Prisma is and whether you should throw away your current tools?\\n\\n\x3c!--truncate--\x3e\\n\\n## TOC\\n\\n1. Prisma basics in 3 minutes\\n2. Things that are mostly the same\\n3. Differentiation\\n4. Closing\\n\\n## Prisma basics in 3 minutes\\n\\nJust before delving into the strategic differences, for the benefit of those unfamiliar with Prisma - here is a quick \'hello-world\' workflow with Prisma ORM. If you\'re already familiar with it - skipping to the next section sounds sensible. Simply put, Prisma dictates 3 key steps to get our ORM code working:\\n\\n**A. Define a model -** Unlike almost any other ORM, Prisma brings a unique language (DSL) for modeling the database-to-code mapping. This proprietary syntax aims to express these models with minimum clutter (i.e., TypeScript generics and verbose code). Worried about having intellisense and validation? A well-crafted vscode extension gets you covered. In the following example, the prisma.schema file describes a DB with an Order table that has a one-to-many relation with a Country table:\\n\\n```prisma\\n// prisma.schema file\\nmodel Order {\\n id Int @id @default(autoincrement())\\n userId Int?\\n paymentTermsInDays Int?\\n deliveryAddress String? @db.VarChar(255)\\n country Country @relation(fields: [countryId], references: [id])\\n countryId Int\\n}\\n\\nmodel Country {\\n id Int @id @default(autoincrement())\\n name String @db.VarChar(255)\\n Order Order[]\\n}\\n```\\n\\n**B. Generate the client code -** Another unusual technique: to get the ORM code ready, one must invoke Prisma\'s CLI and ask for it: \\n\\n```bash\\nnpx prisma generate\\n```\\n\\nAlternatively, if you wish to have your DB ready and the code generated with one command, just fire:\\n\\n```bash\\nnpx prisma migrate deploy\\n```\\n\\nThis will generate migration files that you can execute later in production and also the ORM client code\\n\\n\\nThis will generate migration files that you can execute later in production and the TypeScript ORM code based on the model. The generated code location is defaulted under \'[root]/NODE_MODULES/.prisma/client\'. Every time the model changes, the code must get re-generated again. While most ORMs name this code \'repository\' or \'entity\' or \'active record\', interestingly, Prisma calls it a \'client\'. This shows part of its unique philosophy, which we will explore later\\n\\n**C. All good, use the client to interact with the DB -** The generated client has a rich set of functions and types for your DB interactions. Just import the ORM/client code and use it:\\n\\n```javascript\\nimport { PrismaClient } from \'.prisma/client\';\\n\\nconst prisma = new PrismaClient();\\n// A query example\\nawait prisma.order.findMany({\\n where: {\\n paymentTermsInDays: 30,\\n },\\n orderBy: {\\n id: \'asc\',\\n },\\n });\\n// Use the same client for insertion, deletion, updates, etc\\n```\\n\\nThat\'s the nuts and bolts of Prisma. Is it different and better?\\n\\n## What is the same?\\n\\nWhen comparing options, before outlining differences, it\'s useful to state what is actually similar among these products. Here is a partial list of features that both TypeORM, Sequelize and Prisma support\\n\\n- Casual queries with sorting, filtering, distinct, group by, \'upsert\' (update or create),etc\\n- Raw queries\\n- Full text search\\n- Association/relations of any type (e.g., many to many, self-relation, etc)\\n- Aggregation queries\\n- Pagination\\n- CLI\\n- Transactions\\n- Migration & seeding\\n- Hooks/events (called middleware in Prisma)\\n- Connection pool\\n- Based on various community benchmarks, no dramatic performance differences\\n- All have huge amount of stars and downloads\\n\\nOverall, I found TypeORM and Sequelize to be a little more feature rich. For example, the following features are not supported only in Prisma: GIS queries, DB-level custom constraints, DB replication, soft delete, caching, exclude queries and some more\\n\\nWith that, shall we focus on what really set them apart and make a difference\\n\\n## What is fundamentally different?\\n\\n### 1. Type safety across the board\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** ORM\'s life is not easier since the TypeScript rise, to say the least. The need to support typed models/queries/etc yields a lot of developers sweat. Sequelize, for example, struggles to stabilize a TypeScript interface and, by now offers 3 different syntaxes + one external library ([sequelize-typescript](https://github.com/sequelize/sequelize-typescript)) that offers yet another style. Look at the syntax below, this feels like an afterthought - a library that was not built for TypeScript and now tries to squeeze it in somehow. Despite the major investment, both Sequelize and TypeORM offer only partial type safety. Simple queries do return typed objects, but other common corner cases like attributes/projections leave you with brittle strings. Here are a few examples:\\n\\n\\n```javascript\\n// Sequelize pesky TypeScript interface\\ntype OrderAttributes = {\\n id: number,\\n price: number,\\n // other attributes...\\n};\\n\\ntype OrderCreationAttributes = Optional;\\n\\n//\ud83d\ude2f Isn\'t this a weird syntax?\\nclass Order extends Model, InferCreationAttributes> {\\n declare id: CreationOptional;\\n declare price: number;\\n}\\n```\\n\\n```javascript\\n// Sequelize loose query types\\nawait getOrderModel().findAll({\\n where: { noneExistingField: \'noneExistingValue\' } //\ud83d\udc4d TypeScript will warn here\\n attributes: [\'none-existing-field\', \'another-imaginary-column\'], // No errors here although these columns do not exist\\n include: \'no-such-table\', //\ud83d\ude2f no errors here although this table doesn\'t exist\\n });\\n await getCountryModel().findByPk(\'price\'); //\ud83d\ude2f No errors here although the price column is not a primary key\\n```\\n\\n```javascript\\n// TypeORM loose query\\nconst ordersOnSales: Post[] = await orderRepository.find({\\n where: { onSale: true }, //\ud83d\udc4d TypeScript will warn here\\n select: [\'id\', \'price\'],\\n})\\nconsole.log(ordersOnSales[0].userId); //\ud83d\ude2f No errors here although the \'userId\' column is not part of the returned object\\n```\\n\\nIsn\'t it ironic that a library called **Type**ORM base its queries on strings?\\n\\n\\n**\ud83e\udd14 How Prisma is different:** It takes a totally different approach by generating per-project client code that is fully typed. This client embodies types for everything: every query, relations, sub-queries, everything (except migrations). While other ORMs struggles to infer types from discrete models (including associations that are declared in other files), Prisma\'s offline code generation is easier: It can look through the entire DB relations, use custom generation code and build an almost perfect TypeScript experience. Why \'almost\' perfect? for some reason, Prisma advocates using plain SQL for migrations, which might result in a discrepancy between the code models and the DB schema. Other than that, this is how Prisma\'s client brings end to end type safety:\\n\\n```javascript\\nawait prisma.order.findMany({\\n where: {\\n noneExistingField: 1, //\ud83d\udc4d TypeScript error here\\n },\\n select: {\\n noneExistingRelation: { //\ud83d\udc4d TypeScript error here\\n select: { id: true }, \\n },\\n noneExistingField: true, //\ud83d\udc4d TypeScript error here\\n },\\n });\\n\\n await prisma.order.findUnique({\\n where: { price: 50 }, //\ud83d\udc4d TypeScript error here\\n });\\n```\\n\\n**\ud83d\udcca How important:** TypeScript support across the board is valuable for DX mostly. Luckily, we have another safety net: The project testing. Since tests are mandatory, having build-time type verification is important but not a life saver\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Definitely\\n\\n## 2. Make you forget SQL\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Many avoid ORMs while preferring to interact with the DB using lower-level techniques. One of their arguments is against the efficiency of ORMs: Since the generated queries are not visible immediately to the developers, wasteful queries might get executed unknowingly. While all ORMs provide syntactic sugar over SQL, there are subtle differences in the level of abstraction. The more the ORM syntax resembles SQL, the more likely the developers will understand their own actions\\n\\nFor example, TypeORM\'s query builder looks like SQL broken into convenient functions\\n\\n```javascript\\nawait createQueryBuilder(\'order\')\\n .leftJoinAndSelect(\\n \'order.userId\',\\n \'order.productId\',\\n \'country.name\',\\n \'country.id\'\\n )\\n .getMany();\\n```\\n\\nA developer who read this code \ud83d\udc46 is likely to infer that a *join* query between two tables will get executed\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Prisma\'s mission statement is to simplify DB work, the following statement is taken from their homepage:\\n\\n\\"We designed its API to be intuitive, both for SQL veterans and *developers brand new to databases*\\"\\n\\nBeing ambitious to appeal also to database layman, Prisma builds a syntax with a little bit higher abstraction, for example:\\n\\n```javascript\\nawait prisma.order.findMany({\\n select: {\\n userId: true,\\n productId: true,\\n country: {\\n select: { name: true, id: true },\\n },\\n },\\n});\\n\\n```\\n\\nNo join is reminded here also it fetches records from two related tables (order, and country). Could you guess what SQL is being produced here? how many queries? One right, a simple join? Surprise, actually, two queries are made. Prisma fires one query per-table here, as the join logic happens on the ORM client side (not inside the DB). But why?? in some cases, mostly where there is a lot of repetition in the DB cartesian join, querying each side of the relation is more efficient. But in other cases, it\'s not. Prisma arbitrarily chose what they believe will perform better in *most* cases. I checked, in my case it\'s *slower* than doing a one-join query on the DB side. As a developer, I would miss this deficiency due to the high-level syntax (no join is mentioned). My point is, Prisma sweet and simple syntax might be a bless for developer who are brand new to databases and aim to achieve a working solution in a short time. For the longer term, having full awareness of the DB interactions is helpful, other ORMs encourage this awareness a little better\\n\\n**\ud83d\udcca How important:** Any ORM will hide SQL details from their users - without developer\'s awareness no ORM will save the day\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Not necessarily\\n\\n## 3. Performance\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Speak to an ORM antagonist and you\'ll hear a common sensible argument: ORMs are much slower than a \'raw\' approach. To an extent, this is a legit observation as [most comparisons](https://welingtonfidelis.medium.com/pg-driver-vs-knex-js-vs-sequelize-vs-typeorm-f9ed53e9f802) will show none-negligible differences between raw/query-builder and ORM.\\n\\n![raw is faster d](./pg-driver-is-faster.png)\\n*Example: a direct insert against the PG driver is much shorter [Source](https://welingtonfidelis.medium.com/pg-driver-vs-knex-js-vs-sequelize-vs-typeorm-f9ed53e9f802)* \\n\\n It should also be noted that these benchmarks don\'t tell the entire story - on top of raw queries, every solution must build a mapper layer that maps the raw data to JS objects, nest the results, cast types, and more. This work is included within every ORM but not shown in benchmarks for the raw option. In reality, every team which doesn\'t use ORM would have to build their own small \\"ORM\\", including a mapper, which will also impact performance\\n\\n\\n**\ud83e\udd14 How Prisma is different:** It was my hope to see a magic here, eating the ORM cake without counting the calories, seeing Prisma achieving an almost \'raw\' query speed. I had some good and logical reasons for this hope: Prisma uses a DB client built with Rust. Theoretically, it could serialize to and nest objects faster (in reality, this happens on the JS side). It was also built from the ground up and could build on the knowledge pilled in ORM space for years. Also, since it returns POJOs only (see bullet \'No Active Record here!\') - no time should be spent on decorating objects with ORM fields\\n\\nYou already got it, this hope was not fulfilled. Going with every community benchmark ([one](https://dev.to/josethz00/benchmark-prisma-vs-typeorm-3873), [two](https://github.com/edgedb/imdbench), [three](https://deepkit.io/library)), Prisma at best is not faster than the average ORM. What is the reason? I can\'t tell exactly but it might be due the complicated system that must support Go, future languages, MongoDB and other non-relational DBs\\n\\n![Prisma is not faster](./throughput-benchmark.png)\\n*Example: Prisma is not faster than others. It should be noted that in other benchmarks Prisma scores higher and shows an \'average\' performance [Source](https://github.com/edgedb/imdbench)*\\n\\n**\ud83d\udcca How important:** It\'s expected from ORM users to live peacefully with inferior performance, for many systems it won\'t make a great deal. With that, 10%-30% performance differences between various ORMs are not a key factor\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** No\\n\\n## 4. No active records here!\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Node in its early days was heavily inspired by Ruby (e.g., testing \\"describe\\"), many great patterns were embraced, [Active Record](https://en.wikipedia.org/wiki/Active_record_pattern) is not among the successful ones. What is this pattern about in a nutshell? say you deal with Orders in your system, with Active Record an Order object/class will hold both the entity properties, possible also some of the logic functions and also CRUD functions. Many find this pattern to be awful, why? ideally, when coding some logic/flow, one should not keep her mind busy with side effects and DB narratives. It also might be that accessing some property unconsciously invokes a heavy DB call (i.e., lazy loading). If not enough, in case of heavy logic, unit tests might be in order (i.e., read [\'selective unit tests\'](https://blog.stevensanderson.com/2009/11/04/selective-unit-testing-costs-and-benefits/)) - it\'s going to be much harder to write unit tests against code that interacts with the DB. In fact, all of the respectable and popular architecture (e.g., DDD, clean, 3-tiers, etc) advocate to \'isolate the domain\', separate the core/logic of the system from the surrounding technologies. With all of that said, both TypeORM and Sequelize support the Active Record pattern which is displayed in many examples within their documentation. Both also support other better patterns like the data mapper (see below), but they still open the door for doubtful patterns\\n\\n\\n```javascript\\n// TypeORM active records \ud83d\ude1f\\n\\n@Entity()\\nclass Order extends BaseEntity {\\n @PrimaryGeneratedColumn()\\n id: number\\n\\n @Column()\\n price: number\\n\\n @ManyToOne(() => Product, (product) => product.order)\\n products: Product[]\\n\\n // Other columns here\\n}\\n\\nfunction updateOrder(orderToUpdate: Order){\\n if(orderToUpdate.price > 100){\\n // some logic here\\n orderToUpdate.status = \\"approval\\";\\n orderToUpdate.save(); \\n orderToUpdate.products.forEach((products) =>{ \\n\\n })\\n orderToUpdate.usedConnection = ? \\n }\\n}\\n\\n\\n\\n```\\n\\n**\ud83e\udd14 How Prisma is different:** The better alternative is the data mapper pattern. It acts as a bridge, an adapter, between simple object notations (domain objects with properties) to the DB language, typically SQL. Call it with a plain JS object, POJO, get it saved in the DB. Simple. It won\'t add functions to the result objects or do anything beyond returning pure data, no surprising side effects. In its purest sense, this is a DB-related utility and completely detached from the business logic. While both Sequelize and TypeORM support this, Prisma offers *only* this style - no room for mistakes.\\n\\n\\n```javascript\\n// Prisma approach with a data mapper \ud83d\udc4d\\n\\n// This was generated automatically by Prisma\\ntype Order {\\n id: number\\n\\n price: number\\n\\n products: Product[]\\n\\n // Other columns here\\n}\\n\\nfunction updateOrder(orderToUpdate: Order){\\n if(orderToUpdate.price > 100){\\n orderToUpdate.status = \\"approval\\";\\n prisma.order.update({ where: { id: orderToUpdate.id }, data: orderToUpdate }); \\n // Side effect \ud83d\udc46, but an explicit one. The thoughtful coder will move this to another function. Since it\'s happening outside, mocking is possible \ud83d\udc4d\\n products.forEach((products) =>{ // No lazy loading, the data is already here \ud83d\udc4d\\n\\n })\\n } \\n}\\n```\\n\\n In [Practica.js](https://github.com/practicajs/practica) we take it one step further and put the prisma models within the \\"DAL\\" layer and wrap it with the [repository pattern](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design). You may glimpse [into the code here](https://github.com/practicajs/practica/blob/21ff12ba19cceed9a3735c09d48184b5beb5c410/src/code-templates/services/order-service/domain/new-order-use-case.ts#L21), this is the business flow that calls the DAL layer\\n\\n\\n**\ud83d\udcca How important:** On the one hand, this is a key architectural principle to follow but the other hand most ORMs *allow* doing it right\\n\\n![Medium importance](./high1-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Yes!\\n\\n## 5. Documentation and developer-experience\\n\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** TypeORM and Sequelize documentation is mediocre, though TypeORM is a little better. Based on my personal experience they do get a little better over the years, but still by no mean they deserve to be called \\"good\\" or \\"great\\". For example, if you seek to learn about \'raw queries\' - Sequelize offers [a very short page](https://sequelize.org/docs/v6/core-concepts/raw-queries/) on this matter, TypeORM info is spread in multiple other pages. Looking to learn about pagination? Couldn\'t find Sequelize documents, TypeORM has [some short explanation](https://typeorm.io/select-query-builder#using-pagination), 150 words only\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Prisma documentation rocks! See their documents on similar topics: [raw queries](https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access) and [pagingation](https://www.prisma.io/docs/concepts/components/prisma-client/pagination), thousands of words, and dozens of code examples. The writing itself is also great, feels like some professional writers were involved\\n\\n![Prisma docs are comprehensive](./count-docs.png)\\n \\nThis chart above shows how comprehensive are Prisma docs (Obviously this by itself doesn\'t prove quality)\\n\\n**\ud83d\udcca How important:** Great docs are a key to awareness and avoiding pitfalls\\n\\n![Medium importance](./high1-importance-slider.png)\\n\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** You bet\\n\\n## 6. Observability, metrics, and tracing\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Good chances are (say about 99.9%) that you\'ll find yourself diagnostic slow queries in production or any other DB-related quirks. What can you expect from traditional ORMs in terms of observability? Mostly logging. [Sequelize provides both logging](https://sequelize.org/api/v7/interfaces/queryoptions#benchmark) of query duration and programmatic access to the connection pool state ({size,available,using,waiting}). [TypeORM provides only logging](https://orkhan.gitbook.io/typeorm/docs/logging) of queries that suppress a pre-defined duration threshold. This is better than nothing, but assuming you don\'t read production logs 24/7, you\'d probably need more than logging - an alert to fire when things seem faulty. To achieve this, it\'s your responsibility to bridge this info to your preferred monitoring system. Another logging downside for this sake is verbosity - we need to emit tons of information to the logs when all we really care for is the average duration. Metrics can serve this purpose much better as we\'re about to see soon with Prisma\\n\\nWhat if you need to dig into which specific part of the query is slow? unfortunately, there is no breakdown of the query phases duration - it\'s being left to you as a black-box\\n\\n```javascript\\n// Sequelize - logging various DB information\\n\\n```\\n\\n![Logging query duration](./sequelize-log.png)\\nLogging each query in order to realize trends and anomaly in the monitoring system\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Since Prisma targets also enterprises, it must bring strong ops capabilities. Beautifully, it packs support for both [metrics](https://www.prisma.io/docs/concepts/components/prisma-client/metrics) and [open telemetry tracing](https://www.prisma.io/docs/concepts/components/prisma-client/opentelemetry-tracing)!. For metrics, it generates custom JSON with metric keys and values so anyone can adapt this to any monitoring system (e.g., CloudWatch, statsD, etc). On top of this, it produces out of the box metrics in [Prometheus](https://prometheus.io/) format (one of the most popular monitoring platforms). For example, the metric \'prisma_client_queries_duration_histogram_ms\' provides the average query length in the system overtime. What is even more impressive is the support for open-tracing - it feeds your OpenTelemetry collector with spans that describe the various phases of every query. For example, it might help realize what is the bottleneck in the query pipeline: Is it the DB connection, the query itself or the serialization?\\n\\n![prisma tracing](./trace-diagram.png)\\nPrisma visualizes the various query phases duration with open-telemtry \\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Definitely\\n\\n\\n**\ud83d\udcca How important:** Goes without words how impactful is observability, however filling the gap in other ORM will demand no more than a few days\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n## 7. Continuity - will it be here with us in 2024/2025\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** We live quite peacefully with the risk of one of our dependencies to disappear. With ORM though, this risk demand special attention because our buy-in is higher (i.e., harder to replace) and maintaining it was proven to be harder. Just look at a handful of successful ORMs in the past: objection.js, waterline, bookshelf - all of these respectful project had 0 commits in the past month. The single maintainer of objection.js [announced that he won\'t work the project anymore](https://github.com/Vincit/objection.js/issues/2335). This high churn rate is not surprising given the huge amount of moving parts to maintain, the gazillion corner cases and the modest \'budget\' OSS projects live with. Looking at OpenCollective shows that [Sequelize](https://opencollective.com/sequelize#category-BUDGET) and [TypeORM](https://opencollective.com/typeorm) are funded with ~1500$ month in average. This is barely enough to cover a daily Starbucks cappuccino and croissant (6.95$ x 365) for 5 maintainers. Nothing contrasts this model more than a startup company that just raised its series B - Prisma is [funded with 40,000,000$ (40 millions)](https://www.prisma.io/blog/series-b-announcement-v8t12ksi6x#:~:text=We%20are%20excited%20to%20announce,teams%20%26%20organizations%20in%20this%20article.) and recruited 80 people! Should not this inspire us with high confidence about their continuity? I\'ll surprisingly suggest that quite the opposite is true\\n\\nSee, an OSS ORM has to go over one huge hump, but a startup company must pass through TWO. The OSS project will struggle to achieve the critical mass of features, including some high technical barriers (e.g., TypeScript support, ESM). This typically lasts years, but once it does - a project can focus mostly on maintenance and step out of the danger zone. The good news for TypeORM and Sequelize is that they already did! Both struggled to keep their heads above the water, there were rumors in the past that [TypeORM is not maintained anymore](https://github.com/typeorm/typeorm/issues/3267), but they managed to go through this hump. I counted, both projects had approximately ~2000 PRs in the past 3 years! Going with [repo-tracker](https://repo-tracker.com/r/gh/sequelize/sequelize), each see multiple commits every week. They both have vibrant traction, and the majority of features you would expect from an ORM. TypeORM even supports beyond-the-basics features like multi data source and caching. It\'s unlikely that now, once they reached the promise land - they will fade away. It might happen, there is no guarantee in the OSS galaxy, but the risk is low\\n\\n![One hump](./one-hump.png)\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Prisma a little lags behind in terms of features, but with a budget of 40M$ - there are good reasons to believe that they will pass the first hump, achieving a critical mass of features. I\'m more concerned with the second hump - showing revenues in 2 years or saying goodbye. As a company that is backed by venture capitals - the model is clear and cruel: In order to secure their next round, series B or C (depends whether the seed is counted), there must be a viable and proven business model. How do you \'sell\' ORM? Prisma experiments with multiple products, none is mature yet or being paid for. How big is this risk? According to [this startup companies success statistics](https://spdload.com/blog/startup-success-rate/), \\"About 65% of the Series A startups get series B, while 35% of the companies that get series A fail.\\". Since Prisma already gained a lot of love and adoption from the community, there success chances are higher than the average round A/B company, but even 20% or 10% chances to fade away is concerning\\n\\n> This is terrifying news - companies happily choose a young commercial OSS product without realizing that there are 10-30% chances for this product to disappear\\n\\n\\n![Two humps](./two-humps.png)\\n\\nSome of startup companies who seek a viable business model do not shut the doors rather change the product, the license or the free features. This is not my subjective business analysis, here are few examples: [MongoDB changed their license](https://techcrunch.com/2018/10/16/mongodb-switches-up-its-open-source-license/), this is why the majority had to host their Mongo DB over a single vendor. [Redis did something similar](https://techcrunch.com/2019/02/21/redis-labs-changes-its-open-source-license-again/). What are the chances of Prisma pivoting to another type of product? It actually already happened before, Prisma 1 was mostly about graphQL client and server, [it\'s now retired](https://github.com/prisma/prisma1)\\n\\nIt\'s just fair to mention the other potential path - most round B companies do succeed to qualify for the next round, when this happens even bigger money will be involved in building the \'Ferrari\' of JavaScript ORMs. I\'m surely crossing my fingers for these great people, at the same time we have to be conscious about our choices\\n\\n**\ud83d\udcca How important:** As important as having to code again the entire DB layer in a big system\\n\\n![Medium importance](./high2-importance-slider.png)\\n\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Quite the opposite\\n\\n## Closing - what should you use now?\\n\\nBefore proposing my key take away - which is the primary ORM, let\'s repeat the key learning that were introduced here:\\n\\n1. \ud83e\udd47 Prisma deserves a medal for its awesome DX, documentation, observability support and end-to-end TypeScript coverage\\n2. \ud83e\udd14 There are reasons to be concerned about Prisma\'s business continuity as a young startup without a viable business model. Also Prisma\'s abstract client syntax might blind developers a little more than other ORMs\\n3. \ud83c\udfa9 The contenders, TypeORM and Sequelize, matured and doing quite well: both have merged thousand PRs in the past 3 years to become more stable, they keep introducing new releases (see [repo-tracker](https://repo-tracker.com/r/gh/sequelize/sequelize)), and for now holds more features than Prisma. Also, both show solid performance (for an ORM). Hats off to the maintainers!\\n\\nBased on these observations, which should you pick? which ORM will we use for [practica.js](https://github.com/practicajs/practica)?\\n \\nPrisma is an excellent addition to Node.js ORMs family, but not the hassle-free one tool to rule them all. It\'s a mixed bag of many delicious candies and a few gotchas. Wouldn\'t it grow to tick all the boxes? Maybe, but unlikely. Once built, it\'s too hard to dramatically change the syntax and engine performance. Then, during the writing and speaking with the community, including some Prisma enthusiasts, I realized that it doesn\'t aim to be the can-do-everything \'Ferrari\'. Its positioning seems to resemble more a convenient family car with a solid engine and awesome user experience. In other words, it probably aims for the enterprise space where there is mostly demand for great DX, OK performance, and business-class support\\n\\nIn the end of this journey I see no dominant flawless \'Ferrari\' ORM. I should probably change my perspective: Building ORM for the hectic modern JavaScript ecosystem is 10x harder than building a Java ORM back then in 2001. There is no stain in the shirt, it\'s a cool JavaScript swag. I learned to accept what we have, a rich set of features, tolerable performance, good enough for many systems. Need more? Don\'t use ORM. Nothing is going to change dramatically, it\'s now as good as it can be\\n\\n### When will it shine?\\n\\n**Surely use Prisma under these scenarios -** If your data needs are rather simple; when time-to-market concern takes precedence over the data processing accuracy; when the DB is relatively small; if you\'re a mobile/frontend developer who is doing her first steps in the backend world; when there is a need for business-class support; AND when Prisma\'s long term business continuity risk is a non-issue for you\\n\\n**I\'d probably prefer other options under these conditions -** If the DB layer performance is a major concern; if you\'re savvy backend developer with solid SQL capabilities; when there is a need for fine grain control over the data layer. For all of these cases, Prisma might still work, but my primary choices would be using knex/TypeORM/Sequelize with a data-mapper style\\n\\nConsequently, we love Prisma and add it behind flag (--orm=prisma) to Practica.js. At the same time, until some clouds will disappear, Sequelize will remain our default ORM\\n\\n## Some of my other articles\\n\\n- [Book: Node.js testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [Book: JavaScript testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [Popular Node.js patterns and tools to re-consider](https://practica.dev/blog/popular-nodejs-pattern-and-tools-to-reconsider)\\n- [Practica.js - A Node.js starter](https://github.com/practicajs/practica)\\n- [Node.js best practices](https://github.com/goldbergyoni/nodebestpractices)"},{"id":"popular-nodejs-pattern-and-tools-to-reconsider","metadata":{"permalink":"/blog/popular-nodejs-pattern-and-tools-to-reconsider","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/pattern-to-reconsider/index.md","source":"@site/blog/pattern-to-reconsider/index.md","title":"Popular Node.js patterns and tools to re-consider","description":"Node.js is maturing. Many patterns and frameworks were embraced - it\'s my belief that developers\' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?","date":"2022-08-02T10:00:00.000Z","formattedDate":"August 2, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"nestjs","permalink":"/blog/tags/nestjs"},{"label":"fastify","permalink":"/blog/tags/fastify"},{"label":"passport","permalink":"/blog/tags/passport"},{"label":"dotenv","permalink":"/blog/tags/dotenv"},{"label":"supertest","permalink":"/blog/tags/supertest"},{"label":"practica","permalink":"/blog/tags/practica"},{"label":"testing","permalink":"/blog/tags/testing"}],"readingTime":21.09,"hasTruncateMarker":true,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"}],"frontMatter":{"slug":"popular-nodejs-pattern-and-tools-to-reconsider","date":"2022-08-02T10:00","hide_table_of_contents":true,"title":"Popular Node.js patterns and tools to re-consider","authors":["goldbergyoni"],"tags":["node.js","express","nestjs","fastify","passport","dotenv","supertest","practica","testing"]},"prevItem":{"title":"Is Prisma better than your \'traditional\' ORM?","permalink":"/blog/is-prisma-better-than-your-traditional-orm"},"nextItem":{"title":"Practica.js v0.0.1 is alive","permalink":"/blog/practica-is-alive"}},"content":"Node.js is maturing. Many patterns and frameworks were embraced - it\'s my belief that developers\' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?\\n\\nIn his novel book \'Atomic Habits\' the author James Clear states that:\\n\\n> \\"Mastery is created by habits. However, sometimes when we\'re on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot\\". In other words, practice makes perfect, and bad practices make things worst\\n\\nWe copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change\\n\\nLuckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples. \\n\\nAre those disruptive thoughts surely correct? I\'m not sure. There is one things I\'m sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not \\"don\'t use this tool!\\" but rather becoming familiar with other techniques that, _under some circumstances_ might be a better fit\\n\\n![Animals and frameworks shed their skin](./crab.webp)\\n\\n_The True Crab\'s exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell_\\n\\n\\n## TOC - Patterns to reconsider\\n\\n1. Dotenv\\n2. Calling a service from a controller\\n3. Nest.js dependency injection for all classes\\n4. Passport.js\\n5. Supertest\\n6. Fastify utility decoration\\n7. Logging from a catch clause\\n8. Morgan logger\\n9. NODE_ENV\\n\\n\x3c!--truncate--\x3e\\n## 1. Dotenv as your configuration source\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** A super popular technique in which the app configurable values (e.g., DB user name) are stored in a simple text file. Then, when the app loads, the dotenv library sets all the text file values as environment variables so the code can read this\\n\\n```javascript\\n// .env file\\nUSER_SERVICE_URL=https://users.myorg.com\\n\\n//start.js\\nrequire(\'dotenv\').config();\\n\\n//blog-post-service.js\\nrepository.savePost(post);\\n//update the user number of posts, read the users service URL from an environment variable\\nawait axios.put(`${process.env.USER_SERVICE_URL}/api/user/${post.userId}/incrementPosts`)\\n\\n```\\n\\n**\ud83d\udcca How popular:** 21,806,137 downloads/week!\\n\\n**\ud83e\udd14 Why it might be wrong:** Dotenv is so easy and intuitive to start with, so one might easily overlook fundamental features: For example, it\'s hard to infer the configuration schema and realize the meaning of each key and its typing. Consequently, there is no built-in way to fail fast when a mandatory key is missing - a flow might fail after starting and presenting some side effects (e.g., DB records were already mutated before the failure). In the example above, the blog post will be saved to DB, and only then will the code realize that a mandatory key is missing - This leaves the app hanging in an invalid state. On top of this, in the presence of many keys, it\'s impossible to organize them hierarchically. If not enough, it encourages developers to commit this .env file which might contain production values - this happens because there is no clear way to define development defaults. Teams usually work around this by committing .env.example file and then asking whoever pulls code to rename this file manually. If they remember to of course\\n\\n**\u2600\ufe0f Better alternative:** Some configuration libraries provide out of the box solution to all of these needs. They encourage a clear schema and the possibility to validate early and fail if needed. See [comparison of options here](https://practica.dev/decisions/configuration-library). One of the better alternatives is [\'convict\'](https://github.com/mozilla/node-convict), down below is the same example, this time with Convict, hopefully it\'s better now:\\n\\n```javascript\\n// config.js\\nexport default {\\n userService: {\\n url: {\\n // Hierarchical, documented and strongly typed \ud83d\udc47\\n doc: \\"The URL of the user management service including a trailing slash\\",\\n format: \\"url\\",\\n default: \\"http://localhost:4001\\",\\n nullable: false,\\n env: \\"USER_SERVICE_URL\\",\\n },\\n },\\n //more keys here\\n};\\n\\n//start.js\\nimport convict from \\"convict\\";\\nimport configSchema from \\"config\\";\\nconvict(configSchema);\\n// Fail fast!\\nconvictConfigurationProvider.validate();\\n\\n//blog-post.js\\nrepository.savePost(post);\\n// Will never arrive here if the URL is not set\\nawait axios.put(\\n `${convict.get(userService.url)}/api/user/${post.userId}/incrementPosts`\\n);\\n```\\n\\n## 2. Calling a \'fat\' service from the API controller\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Consider a reader of our code who wishes to understand the entire _high-level_ flow or delve into a very _specific_ part. She first lands on the API controller, where requests start. Unlike what its name implies, this controller layer is just an adapter and kept really thin and straightforward. Great thus far. Then the controller calls a big \'service\' with thousands of lines of code that represent the entire logic\\n\\n```javascript\\n// user-controller\\nrouter.post(\'/\', async (req, res, next) => {\\n await userService.add(req.body);\\n // Might have here try-catch or error response logic\\n}\\n\\n// user-service\\nexports function add(newUser){\\n // Want to understand quickly? Need to understand the entire user service, 1500 loc\\n // It uses technical language and reuse narratives of other flows\\n this.copyMoreFieldsToUser(newUser)\\n const doesExist = this.updateIfAlreadyExists(newUser)\\n if(!doesExist){\\n addToCache(newUser);\\n }\\n // 20 more lines that demand navigating to other functions in order to get the intent\\n}\\n\\n\\n```\\n\\n**\ud83d\udcca How popular:** It\'s hard to pull solid numbers here, I could confidently say that in _most_ of the app that I see, this is the case\\n\\n**\ud83e\udd14 Why it might be wrong:** We\'re here to tame complexities. One of the useful techniques is deferring a complexity to the later stage possible. In this case though, the reader of the code (hopefully) starts her journey through the tests and the controller - things are simple in these areas. Then, as she lands on the big service - she gets tons of complexity and small details, although she is focused on understanding the overall flow or some specific logic. This is **unnecessary** complexity\\n\\n**\u2600\ufe0f Better alternative:** The controller should call a particular type of service, a **use-case** , which is responsible for _summarizing_ the flow in a business and simple language. Each flow/feature is described using a use-case, each contains 4-10 lines of code, that tell the story without technical details. It mostly orchestrates other small services, clients, and repositories that hold all the implementation details. With use cases, the reader can grasp the high-level flow easily. She can now **choose** where she would like to focus. She is now exposed only to **necessary** complexity. This technique also encourages partitioning the code to the smaller object that the use-case orchestrates. Bonus: By looking at coverage reports, one can tell which features are covered, not just files/functions\\n\\nThis idea by the way is formalized in the [\'clean architecture\' book](https://www.bookdepository.com/Clean-Architecture-Robert-Martin/9780134494166?redirected=true&utm_medium=Google&utm_campaign=Base1&utm_source=IL&utm_content=Clean-Architecture&selectCurrency=ILS&w=AFF9AU99ZB4MTDA8VTRQ&gclid=Cj0KCQjw3eeXBhD7ARIsAHjssr92kqLn60dnfQCLjbkaqttdgvhRV5dqKtnY680GCNDvKp-16HtZp24aAg6GEALw_wcB) - I\'m not a big fan of \'fancy\' architectures, but see - it\'s worth cherry-picking techniques from every source. You may walk-through our [Node.js best practices starter, practica.js](https://github.com/practicajs/practica), and examine the use-cases code\\n\\n```javascript\\n// add-order-use-case.js\\nexport async function addOrder(newOrder: addOrderDTO) {\\n orderValidation.assertOrderIsValid(newOrder);\\n const userWhoOrdered = await userServiceClient.getUserWhoOrdered(\\n newOrder.userId\\n );\\n paymentTermsService.assertPaymentTerms(\\n newOrder.paymentTermsInDays,\\n userWhoOrdered.terms\\n );\\n\\n const response = await orderRepository.addOrder(newOrder);\\n\\n return response;\\n}\\n```\\n\\n## 3. Nest.js: Wire _everything_ with dependency injection\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** If you\'re doing Nest.js, besides having a powerful framework in your hands, you probably use DI for _everything_ and make every class injectable. Say you have a weather-service that depends upon humidity-service, and **there is no requirement to swap** the humidity-service with alternative providers. Nevertheless, you inject humidity-service into the weather-service. It becomes part of your development style, \\"why not\\" you think - I may need to stub it during testing or replace it in the future\\n\\n```typescript\\n// humidity-service.ts - not customer facing\\n@Injectable()\\nexport class GoogleHumidityService {\\n\\n async getHumidity(when: Datetime): Promise {\\n // Fetches from some specific cloud service\\n }\\n}\\n\\n// weather-service.ts - customer facing\\nimport { GoogleHumidityService } from \'./humidity-service.ts\';\\n\\nexport type weatherInfo{\\n temperature: number,\\n humidity: number\\n}\\n\\nexport class WeatherService {\\n constructor(private humidityService: GoogleHumidityService) {}\\n\\n async GetWeather(when: Datetime): Promise {\\n // Fetch temperature from somewhere and then humidity from GoogleHumidityService\\n }\\n}\\n\\n// app.module.ts\\n@Module({\\n providers: [GoogleHumidityService, WeatherService],\\n})\\nexport class AppModule {}\\n```\\n\\n**\ud83d\udcca How popular:** No numbers here but I could confidently say that in _all_ of the Nest.js app that I\'ve seen, this is the case. In the popular [\'nestjs-realworld-example-ap[p\'](](https://github.com/lujakob/nestjs-realworld-example-app)) all the services are \'injectable\'\\n\\n**\ud83e\udd14 Why it might be wrong:** Dependency injection is not a priceless coding style but a pattern you should pull in the right moment, like any other pattern. Why? Because any pattern has a price. What price, you ask? First, encapsulation is violated. Clients of the weather-service are now aware that other providers are being used _internally_. Some clients may get tempted to override providers also it\'s not under their responsibility. Second, it\'s another layer of complexity to learn, maintain, and one more way to shoot yourself in the legs. StackOverflow owes some of its revenues to Nest.js DI - plenty of discussions try to solve this puzzle (e.g. did you know that in case of circular dependencies the order of imports matters?). Third, there is the performance thing - Nest.js, for example struggled to provide a decent start time for serverless environments and had to introduce [lazy loaded modules](https://docs.nestjs.com/fundamentals/lazy-loading-modules). Don\'t get me wrong, **in some cases**, there is a good case for DI: When a need arises to decouple a dependency from its caller, or to allow clients to inject custom implementations (e.g., the strategy pattern). **In such case**, when there is a value, you may consider whether the _value of DI is worth its price_. If you don\'t have this case, why pay for nothing?\\n\\nI recommend reading the first paragraphs of this blog post [\'Dependency Injection is EVIL\'](https://www.tonymarston.net/php-mysql/dependency-injection-is-evil.html) (and absolutely don\'t agree with this bold words)\\n\\n**\u2600\ufe0f Better alternative:** \'Lean-ify\' your engineering approach - avoid using any tool unless it serves a real-world need immediately. Start simple, a dependent class should simply import its dependency and use it - Yeah, using the plain Node.js module system (\'require\'). Facing a situation when there is a need to factor dynamic objects? There are a handful of simple patterns, simpler than DI, that you should consider, like \'if/else\', factory function, and more. Are singletons requested? Consider techniques with lower costs like the module system with factory function. Need to stub/mock for testing? Monkey patching might be better than DI: better clutter your test code a bit than clutter your production code. Have a strong need to hide from an object where its dependencies are coming from? You sure? Use DI!\\n\\n```typescript\\n// humidity-service.ts - not customer facing\\nexport async function getHumidity(when: Datetime): Promise {\\n // Fetches from some specific cloud service\\n}\\n\\n// weather-service.ts - customer facing\\nimport { getHumidity } from \\"./humidity-service.ts\\";\\n\\n// \u2705 No wiring is happening externally, all is flat and explicit. Simple\\nexport async function getWeather(when: Datetime): Promise {\\n // Fetch temperature from somewhere and then humidity from GoogleHumidityService\\n // Nobody needs to know about it, its an implementation details\\n await getHumidity(when);\\n}\\n```\\n\\n___\\n\\n## 1 min pause: A word or two about me, the author\\n\\nMy name is Yoni Goldberg, I\'m a Node.js developer and consultant. I wrote few code-books like [JavaScript testing best practices](https://github.com/goldbergyoni/javascript-testing-best-practices) and [Node.js best practices](https://github.com/goldbergyoni/nodebestpractices) (100,000 stars \u2728\ud83e\udd79). That said, my best guide is [Node.js testing practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices) which only few read \ud83d\ude1e. I shall release [an advanced Node.js testing course soon](https://testjavascript.com/) and also hold workshops for teams. I\'m also a core maintainer of [Practica.js](https://github.com/practicajs/practica) which is a Node.js starter that creates a production-ready example Node Monorepo solution that is based on the standards and simplicity. It might be your primary option when starting a new Node.js solution\\n\\n___\\n\\n## 4. Passport.js for token authentication\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Commonly, you\'re in need to issue or/and authenticate JWT tokens. Similarly, you might need to allow login from _one_ single social network like Google/Facebook. When faced with these kinds of needs, Node.js developers rush to the glorious library [Passport.js](https://www.passportjs.org/) like butterflies are attracted to light\\n\\n**\ud83d\udcca How popular:** 1,389,720 weekly downloads\\n\\n**\ud83e\udd14 Why it might be wrong:** When tasked with guarding your routes with JWT token - you\'re just a few lines of code shy from ticking the goal. Instead of messing up with a new framework, instead of introducing levels of indirections (you call passport, then it calls you), instead of spending time learning new abstractions - use a JWT library directly. Libraries like [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) or [fast-jwt](https://github.com/nearform/fast-jwt) are simple and well maintained. Have concerns with the security hardening? Good point, your concerns are valid. But would you not get better hardening with a direct understanding of your configuration and flow? Will hiding things behind a framework help? Even if you prefer the hardening of a battle-tested framework, Passport doesn\'t handle a handful of security risks like secrets/token, secured user management, DB protection, and more. My point, you probably anyway need fully-featured user and authentication management platforms. Various cloud services and OSS projects, can tick all of those security concerns. Why then start in the first place with a framework that doesn\'t satisfy your security needs? It seems like many who opt for Passport.js are not fully aware of which needs are satisfied and which are left open. All of that said, Passport definitely shines when looking for a quick way to support _many_ social login providers\\n\\n**\u2600\ufe0f Better alternative:** Is token authentication in order? These few lines of code below might be all you need. You may also glimpse into [Practica.js wrapper around these libraries](https://github.com/practicajs/practica/tree/main/src/code-templates/libraries/jwt-token-verifier). A real-world project at scale typically need more: supporting async JWT [(JWKS)](https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets), securely manage and rotate the secrets to name a few examples. In this case, OSS solution like [keycloak (https://github.com/keycloak/keycloak) or commercial options like Auth0[https://github.com/auth0] are alternatives to consider\\n\\n```javascript\\n// jwt-middleware.js, a simplified version - Refer to Practica.js to see some more corner cases\\nconst middleware = (req, res, next) => {\\n if(!req.headers.authorization){\\n res.sendStatus(401)\\n }\\n\\n jwt.verify(req.headers.authorization, options.secret, (err: any, jwtContent: any) => {\\n if (err) {\\n return res.sendStatus(401);\\n }\\n\\n req.user = jwtContent.data;\\n\\n next();\\n });\\n```\\n\\n## 5. Supertest for integration/API testing\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** When testing against an API (i.e., component, integration, E2E tests), the library [supertest](https://www.npmjs.com/package/supertest) provides a sweet syntax that can both detect the web server address, make HTTP call and also assert on the response. Three in one\\n\\n```javascript\\ntest(\\"When adding invalid user, then the response is 400\\", (done) => {\\n const request = require(\\"supertest\\");\\n const app = express();\\n // Arrange\\n const userToAdd = {\\n name: undefined,\\n };\\n\\n // Act\\n request(app)\\n .post(\\"/user\\")\\n .send(userToAdd)\\n .expect(\\"Content-Type\\", /json/)\\n .expect(400, done);\\n\\n // Assert\\n // We already asserted above \u261d\ud83c\udffb as part of the request\\n});\\n```\\n\\n**\ud83d\udcca How popular:** 2,717,744 weekly downloads\\n\\n**\ud83e\udd14 Why it might be wrong:** You already have your assertion library (Jest? Chai?), it has a great error highlighting and comparison - you trust it. Why code some tests using another assertion syntax? Not to mention, Supertest\'s assertion errors are not as descriptive as Jest and Chai. It\'s also cumbersome to mix HTTP client + assertion library instead of choosing the best for each mission. Speaking of the best, there are more standard, popular, and better-maintained HTTP clients (like fetch, axios and other friends). Need another reason? Supertest might encourage coupling the tests to Express as it offers a constructor that gets an Express object. This constructor infers the API address automatically (useful when using dynamic test ports). This couples the test to the implementation and won\'t work in the case where you wish to run the same tests against a remote process (the API doesn\'t live with the tests). My repository [\'Node.js testing best practices\'](https://github.com/testjavascript/nodejs-integration-tests-best-practices) holds examples of how tests can infer the API port and address\\n\\n**\u2600\ufe0f Better alternative:** A popular and standard HTTP client library like Node.js Fetch or Axios. In [Practica.js](https://github.com/practicajs/practica) (a Node.js starter that packs many best practices) we use Axios. It allows us to configure a HTTP client that is shared among all the tests: We bake inside a JWT token, headers, and a base URL. Another good pattern that we look at, is making each Microservice generate HTTP client library for its consumers. This brings strong-type experience to the clients, synchronizes the provider-consumer versions and as a bonus - The provider can test itself with the same library that its consumers are using\\n\\n```javascript\\ntest(\\"When adding invalid user, then the response is 400 and includes a reason\\", (done) => {\\n const app = express();\\n // Arrange\\n const userToAdd = {\\n name: undefined,\\n };\\n\\n // Act\\n const receivedResponse = axios.post(\\n `http://localhost:${apiPort}/user`,\\n userToAdd\\n );\\n\\n // Assert\\n // \u2705 Assertion happens in a dedicated stage and a dedicated library\\n expect(receivedResponse).toMatchObject({\\n status: 400,\\n data: {\\n reason: \\"no-name\\",\\n },\\n });\\n});\\n```\\n\\n## 6. Fastify decorate for non request/web utilities\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** [Fastify](https://github.com/fastify/fastify) introduces great patterns. Personally, I highly appreciate how it preserves the simplicity of Express while bringing more batteries. One thing that got me wondering is the \'decorate\' feature which allows placing common utilities/services inside a widely accessible container object. I\'m referring here specifically to the case where a cross-cutting concern utility/service is being used. Here is an example:\\n\\n```javascript\\n// An example of a utility that is cross-cutting-concern. Could be logger or anything else\\nfastify.decorate(\'metricsService\', function (name) {\\n fireMetric: () => {\\n // My code that sends metrics to the monitoring system\\n }\\n})\\n\\nfastify.get(\'/api/orders\', async function (request, reply) {\\n this.metricsService.fireMetric({name: \'new-request\'})\\n // Handle the request\\n})\\n\\n// my-business-logic.js\\nexports function calculateSomething(){\\n // How to fire a metric?\\n}\\n```\\n\\nIt should be noted that \'decoration\' is also used to place values (e.g., user) inside a request - this is a slightly different case and a sensible one\\n\\n**\ud83d\udcca How popular:** Fastify has 696,122 weekly download and growing rapidly. The decorator concept is part of the framework\'s core\\n\\n**\ud83e\udd14 Why it might be wrong:** Some services and utilities serve cross-cutting-concern needs and should be accessible from other layers like domain (i.e, business logic, DAL). When placing utilities inside this object, the Fastify object might not be accessible to these layers. You probably don\'t want to couple your web framework with your business logic: Consider that some of your business logic and repositories might get invoked from non-REST clients like CRON, MQ, and similar - In these cases, Fastify won\'t get involved at all so better not trust it to be your service locator\\n\\n**\u2600\ufe0f Better alternative:** A good old Node.js module is a standard way to expose and consume functionality. Need a singleton? Use the module system caching. Need to instantiate a service in correlation with a Fastify life-cycle hook (e.g., DB connection on start)? Call it from that Fastify hook. In the rare case where a highly dynamic and complex instantiation of dependencies is needed - DI is also a (complex) option to consider\\n\\n```javascript\\n// \u2705 A simple usage of good old Node.js modules\\n// metrics-service.js\\n\\nexports async function fireMetric(name){\\n // My code that sends metrics to the monitoring system\\n}\\n\\nimport {fireMetric} from \'./metrics-service.js\'\\n\\nfastify.get(\'/api/orders\', async function (request, reply) {\\n metricsService.fireMetric({name: \'new-request\'})\\n})\\n\\n// my-business-logic.js\\nexports function calculateSomething(){\\n metricsService.fireMetric({name: \'new-request\'})\\n}\\n```\\n\\n## 7. Logging from a catch clause\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** You catch an error somewhere deep in the code (not on the route level), then call logger.error to make this error observable. Seems simple and necessary\\n\\n```javascript\\ntry{\\n axios.post(\'https://thatService.io/api/users);\\n}\\ncatch(error){\\n logger.error(error, this, {operation: addNewOrder});\\n}\\n```\\n\\n**\ud83d\udcca How popular:** Hard to put my hands on numbers but it\'s quite popular, right?\\n\\n**\ud83e\udd14 Why it might be wrong:** First, errors should get handled/logged in a central location. Error handling is a critical path. Various catch clauses are likely to behave differently without a centralized and unified behavior. For example, a request might arise to tag all errors with certain metadata, or on top of logging, to also fire a monitoring metric. Applying these requirements in ~100 locations is not a walk in the park. Second, catch clauses should be minimized to particular scenarios. By default, the natural flow of an error is bubbling down to the route/entry-point - from there, it will get forwarded to the error handler. Catch clauses are more verbose and error-prone - therefore it should serve two very specific needs: When one wishes to change the flow based on the error or enrich the error with more information (which is not the case in this example)\\n\\n**\u2600\ufe0f Better alternative:** By default, let the error bubble down the layers and get caught by the entry-point global catch (e.g., Express error middleware). In cases when the error should trigger a different flow (e.g., retry) or there is value in enriching the error with more context - use a catch clause. In this case, ensure the .catch code also reports to the error handler\\n\\n```javascript\\n// A case where we wish to retry upon failure\\ntry{\\n axios.post(\'https://thatService.io/api/users);\\n}\\ncatch(error){\\n // \u2705 A central location that handles error\\n errorHandler.handle(error, this, {operation: addNewOrder});\\n callTheUserService(numOfRetries++);\\n}\\n```\\n\\n## 8. Use Morgan logger for express web requests\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** In many web apps, you are likely to find a pattern that is being copy-pasted for ages - Using Morgan logger to log requests information:\\n\\n```javascript\\nconst express = require(\\"express\\");\\nconst morgan = require(\\"morgan\\");\\n\\nconst app = express();\\n\\napp.use(morgan(\\"combined\\"));\\n```\\n\\n**\ud83d\udcca How popular:** 2,901,574 downloads/week\\n\\n**\ud83e\udd14 Why it might be wrong:** Wait a second, you already have your main logger, right? Is it Pino? Winston? Something else? Great. Why deal with and configure yet another logger? I do appreciate the HTTP domain-specific language (DSL) of Morgan. The syntax is sweet! But does it justify having two loggers?\\n\\n**\u2600\ufe0f Better alternative:** Put your chosen logger in a middleware and log the desired request/response properties:\\n\\n```javascript\\n// \u2705 Use your preferred logger for all the tasks\\nconst logger = require(\\"pino\\")();\\napp.use((req, res, next) => {\\n res.on(\\"finish\\", () => {\\n logger.info(`${req.url} ${res.statusCode}`); // Add other properties here\\n });\\n next();\\n});\\n```\\n\\n## 9. Having conditional code based on `NODE_ENV` value\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** To differentiate between development vs production configuration, it\'s common to set the environment variable NODE_ENV with \\"production|test\\". Doing so allows the various tooling to act differently. For example, some templating engines will cache compiled templates only in production. Beyond tooling, custom applications use this to specify behaviours that are unique to the development or production environment:\\n\\n```javascript\\nif (process.env.NODE_ENV === \\"production\\") {\\n // This is unlikely to be tested since test runner usually set NODE_ENV=test\\n setLogger({ stdout: true, prettyPrint: false });\\n // If this code branch above exists, why not add more production-only configurations:\\n collectMetrics();\\n} else {\\n setLogger({ splunk: true, prettyPrint: true });\\n}\\n```\\n\\n**\ud83d\udcca How popular:** 5,034,323 code results in GitHub when searching for \\"NODE_ENV\\". It doesn\'t seem like a rare pattern\\n\\n**\ud83e\udd14 Why it might be wrong:** Anytime your code checks whether it\'s production or not, this branch won\'t get hit by default in some test runner (e.g., Jest set `NODE_ENV=test`). In _any_ test runner, the developer must remember to test for each possible value of this environment variable. In the example above, `collectMetrics()` will be tested for the first time in production. Sad smiley. Additionally, putting these conditions opens the door to add more differences between production and the developer machine - when this variable and conditions exists, a developer gets tempted to put some logic for production only. Theoretically, this can be tested: one can set `NODE_ENV = \\"production\\"` in testing and cover the production branches (if she remembers...). But then, if you can test with `NODE_ENV=\'production\'`, what\'s the point in separating? Just consider everything to be \'production\' and avoid this error-prone mental load\\n\\n**\u2600\ufe0f Better alternative:** Any code that was written by us, must be tested. This implies avoiding any form of if(production)/else(development) conditions. Wouldn\'t anyway developers machine have different surrounding infrastructure than production (e.g., logging system)? They do, the environments are quite difference, but we feel comfortable with it. These infrastructural things are battle-tested, extraneous, and not part of our code. To keep the same code between dev/prod and still use different infrastructure - we put different values in the configuration (not in the code). For example, a typical logger emits JSON in production but in a development machine it emits \'pretty-print\' colorful lines. To meet this, we set ENV VAR that tells whether what logging style we aim for:\\n\\n```javascript\\n//package.json\\n\\"scripts\\": {\\n \\"start\\": \\"LOG_PRETTY_PRINT=false index.js\\",\\n \\"test\\": \\"LOG_PRETTY_PRINT=true jest\\"\\n}\\n\\n//index.js\\n//\u2705 No condition, same code for all the environments. The variations are defined externally in config or deployment files\\nsetLogger({prettyPrint: process.env.LOG_PRETTY_PRINT})\\n```\\n\\n## Closing\\n\\nI hope that these thoughts, at least one of them, made you re-consider adding a new technique to your toolbox. In any case, let\'s keep our community vibrant, disruptive and kind. Respectful discussions are almost as important as the event loop. Almost.\\n\\n## Some of my other articles\\n\\n- [Book: Node.js testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [Book: JavaScript testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [How to be a better Node.js developer in 2020](https://yonigoldberg.medium.com/20-ways-to-become-a-better-node-js-developer-in-2020-d6bd73fcf424). The 2023 version is coming soon\\n- [Practica.js - A Node.js starter](https://github.com/practicajs/practica)\\n- [Node.js best practices](https://github.com/goldbergyoni/nodebestpractices)"},{"id":"practica-is-alive","metadata":{"permalink":"/blog/practica-is-alive","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/practica-is-alive/index.md","source":"@site/blog/practica-is-alive/index.md","title":"Practica.js v0.0.1 is alive","description":"\ud83e\udd73 We\'re thrilled to launch the very first version of Practica.js.","date":"2022-07-15T10:00:00.000Z","formattedDate":"July 15, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"fastify","permalink":"/blog/tags/fastify"}],"readingTime":1.21,"hasTruncateMarker":false,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"}],"frontMatter":{"slug":"practica-is-alive","date":"2022-07-15T10:00","hide_table_of_contents":true,"title":"Practica.js v0.0.1 is alive","authors":["goldbergyoni"],"tags":["node.js","express","fastify"]},"prevItem":{"title":"Popular Node.js patterns and tools to re-consider","permalink":"/blog/popular-nodejs-pattern-and-tools-to-reconsider"}},"content":"\ud83e\udd73 We\'re thrilled to launch the very first version of Practica.js.\\n\\n## What is Practica is one paragraph\\n\\nAlthough Node.js has great frameworks \ud83d\udc9a, they were never meant to be production ready immediately. Practica.js aims to bridge the gap. Based on your preferred framework, we generate some example code that demonstrates a full workflow, from API to DB, that is packed with good practices. For example, we include a hardened dockerfile, N-Tier folder structure, great testing templates, and more. This saves a great deal of time and can prevent painful mistakes. All decisions made are [neatly and thoughtfully documented](./decisions/index). We strive to keep things as simple and standard as possible and base our work off the popular guide: [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices).\\n\\nYour developer experience would look as follows: Generate our starter using the CLI and get an example Node.js solution. This solution is a typical Monorepo setup with an example Microservice and libraries. All is based on super-popular libraries that we merely stitch together. It also constitutes tons of optimization - linters, libraries, Monorepo configuration, tests and much more. Inside the example Microservice you\'ll find an example flow, from API to DB. Based on this, you can modify the entity and DB fields and build you app. \\n\\n## 90 seconds video\\n\\n\\n\\n## How to get started\\n\\nTo get up to speed quickly, read our [getting started guide](https://practica.dev/the-basics/getting-started-quickly)."}]}')}}]); \ No newline at end of file diff --git a/assets/js/b2f554cd.d06d85c3.js b/assets/js/b2f554cd.d06d85c3.js deleted file mode 100644 index d9c12650..00000000 --- a/assets/js/b2f554cd.d06d85c3.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[1477],{10:e=>{e.exports=JSON.parse('{"blogPosts":[{"id":"testing-the-dark-scenarios-of-your-nodejs-application","metadata":{"permalink":"/blog/testing-the-dark-scenarios-of-your-nodejs-application","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/crucial-tests/index.md","source":"@site/blog/crucial-tests/index.md","title":"Testing the dark scenarios of your Node.js application","description":"Where the dead-bodies are covered","date":"2023-07-07T11:00:00.000Z","formattedDate":"July 7, 2023","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"testing","permalink":"/blog/tags/testing"},{"label":"component-test","permalink":"/blog/tags/component-test"},{"label":"fastify","permalink":"/blog/tags/fastify"},{"label":"unit-test","permalink":"/blog/tags/unit-test"},{"label":"integration","permalink":"/blog/tags/integration"},{"label":"nock","permalink":"/blog/tags/nock"}],"readingTime":19.875,"hasTruncateMarker":false,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"},{"name":"Raz Luvaton","title":"Practica.js core maintainer","url":"https://github.com/rluvaton","imageURL":"https://avatars.githubusercontent.com/u/16746759?v=4","key":"razluvaton"}],"frontMatter":{"slug":"testing-the-dark-scenarios-of-your-nodejs-application","date":"2023-07-07T11:00","hide_table_of_contents":true,"title":"Testing the dark scenarios of your Node.js application","authors":["goldbergyoni","razluvaton"],"tags":["node.js","testing","component-test","fastify","unit-test","integration","nock"]},"nextItem":{"title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","permalink":"/blog/monorepo-backend"}},"content":"## Where the dead-bodies are covered\\n\\nThis post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked\\n\\nSome context first: How do we test a modern backend? With [the testing diamond](https://ritesh-kapoor.medium.com/testing-automation-what-are-pyramids-and-diamonds-67494fec7c55), of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we\'ve also written [a guide with 50 best practices for integration tests in Node.js](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n\\nBut there is a pitfall: most developers write _only_ semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don\'t simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime\\n\\n![The hidden corners](./the-hidden-corners.png)\\n\\nHere are a handful of examples that might open your mind to a whole new class of risks and tests\\n\\n## \ud83e\udddf\u200d\u2640\ufe0f The zombie process test\\n\\n**\ud83d\udc49What & so what? -** In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see [readiness probe](https://komodor.com/learn/kubernetes-readiness-probes-a-practical-guide/#:~:text=A%20readiness%20probe%20allows%20Kubernetes,on%20deletion%20of%20a%20pod.)). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a \'zombie process\'. In this scenario, the runtime platform won\'t realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!\\n\\n**\ud83d\udcdd Code**\\n\\n**Code under test, api.js:**\\n\\n```javascript\\n// A common express server initialization\\nconst startWebServer = () => {\\n return new Promise((resolve, reject) => {\\n try {\\n // A typical Express setup\\n expressApp = express();\\n defineRoutes(expressApp); // a function that defines all routes\\n expressApp.listen(process.env.WEB_SERVER_PORT);\\n } catch (error) {\\n //log here, fire a metric, maybe even retry and finally:\\n process.exit();\\n }\\n });\\n};\\n```\\n\\n**The test:**\\n\\n```javascript\\nconst api = require(\'./entry-points/api\'); // our api starter that exposes \'startWebServer\' function\\nconst sinon = require(\'sinon\'); // a mocking library\\n\\ntest(\'When an error happens during the startup phase, then the process exits\', async () => {\\n // Arrange\\n const processExitListener = sinon.stub(process, \'exit\');\\n // \ud83d\udc47 Choose a function that is part of the initialization phase and make it fail\\n sinon\\n .stub(routes, \'defineRoutes\')\\n .throws(new Error(\'Cant initialize connection\'));\\n\\n // Act\\n await api.startWebServer();\\n\\n // Assert\\n expect(processExitListener.called).toBe(true);\\n});\\n```\\n\\n## \ud83d\udc40 The observability test\\n\\n**\ud83d\udc49What & why -** For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error **correctly observable**. In plain words, ensuring that it\'s being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, _including stack trace_, cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn\'t care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\ntest(\'When exception is throw during request, Then logger reports the mandatory fields\', async () => {\\n //Arrange\\n const orderToAdd = {\\n userId: 1,\\n productId: 2,\\n status: \'approved\',\\n };\\n const metricsExporterDouble = sinon.stub(metricsExporter, \'fireMetric\');\\n sinon\\n .stub(OrderRepository.prototype, \'addOrder\')\\n .rejects(new AppError(\'saving-failed\', \'Order could not be saved\', 500));\\n const loggerDouble = sinon.stub(logger, \'error\');\\n\\n //Act\\n await axiosAPIClient.post(\'/order\', orderToAdd);\\n\\n //Assert\\n expect(loggerDouble).toHaveBeenCalledWith({\\n name: \'saving-failed\',\\n status: 500,\\n stack: expect.any(String),\\n message: expect.any(String),\\n });\\n expect(\\n metricsExporterDouble).toHaveBeenCalledWith(\'error\', {\\n errorName: \'example-error\',\\n })\\n});\\n```\\n\\n## \ud83d\udc7d The \'unexpected visitor\' test - when an uncaught exception meets our code\\n\\n**\ud83d\udc49What & why -** A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let\'s focus on the 2nd assumption: it\'s common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on(\'error\', ...). To name a few examples. These errors will find their way to the global process.on(\'uncaughtException\') handler, **hopefully if your code subscribed**. How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here\'s a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is \'borderless\', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here\'s an example:\\n\\nresearches says that, rejection\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\ntest(\'When an unhandled exception is thrown, then process stays alive and the error is logged\', async () => {\\n //Arrange\\n const loggerDouble = sinon.stub(logger, \'error\');\\n const processExitListener = sinon.stub(process, \'exit\');\\n const errorToThrow = new Error(\'An error that wont be caught \ud83d\ude33\');\\n\\n //Act\\n process.emit(\'uncaughtException\', errorToThrow); //\ud83d\udc48 Where the magic is\\n\\n // Assert\\n expect(processExitListener.called).toBe(false);\\n expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);\\n});\\n```\\n\\n## \ud83d\udd75\ud83c\udffc The \'hidden effect\' test - when the code should not mutate at all\\n\\n**\ud83d\udc49What & so what -** In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn\'t have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn\'t guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you\'re not cleaning the DB often (like me, but that\'s another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\nit(\'When adding an invalid order, then it returns 400 and NOT retrievable\', async () => {\\n //Arrange\\n const orderToAdd = {\\n userId: 1,\\n mode: \'draft\',\\n externalIdentifier: uuid(), //no existing record has this value\\n };\\n\\n //Act\\n const { status: addingHTTPStatus } = await axiosAPIClient.post(\\n \'/order\',\\n orderToAdd\\n );\\n\\n //Assert\\n const { status: fetchingHTTPStatus } = await axiosAPIClient.get(\\n `/order/externalIdentifier/${orderToAdd.externalIdentifier}`\\n ); // Trying to get the order that should have failed\\n expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({\\n addingHTTPStatus: 400,\\n fetchingHTTPStatus: 404,\\n });\\n // \ud83d\udc46 Check that no such record exists\\n});\\n```\\n\\n## \ud83e\udde8 The \'overdoing\' test - when the code should mutate but it\'s doing too much\\n\\n**\ud83d\udc49What & why -** This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here\'s a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more \'control\' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\ntest(\'When deleting an existing order, Then it should NOT be retrievable\', async () => {\\n // Arrange\\n const orderToDelete = {\\n userId: 1,\\n productId: 2,\\n };\\n const deletedOrder = (await axiosAPIClient.post(\'/order\', orderToDelete)).data\\n .id; // We will delete this soon\\n const orderNotToBeDeleted = orderToDelete;\\n const notDeletedOrder = (\\n await axiosAPIClient.post(\'/order\', orderNotToBeDeleted)\\n ).data.id; // We will not delete this\\n\\n // Act\\n await axiosAPIClient.delete(`/order/${deletedOrder}`);\\n\\n // Assert\\n const { status: getDeletedOrderStatus } = await axiosAPIClient.get(\\n `/order/${deletedOrder}`\\n );\\n const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(\\n `/order/${notDeletedOrder}`\\n );\\n expect(getNotDeletedOrderStatus).toBe(200);\\n expect(getDeletedOrderStatus).toBe(404);\\n});\\n```\\n\\n## \ud83d\udd70 The \'slow collaborator\' test - when the other HTTP service times out\\n\\n**\ud83d\udc49What & why -** When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it\'s harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like [nock](https://github.com/nock/nock) or [wiremock](https://wiremock.org/). These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available **in production**, what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can\'t wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other \'chaotic\' scenarios. Question left is how to simulate slow response without having slow tests? You may use [fake timers](https://sinonjs.org/releases/latest/fake-timers/) and trick the system into believing as few seconds passed in a single tick. If you\'re using [nock](https://github.com/nock/nock), it offers an interesting feature to simulate timeouts **quickly**: the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting\\n\\n**\ud83d\udcdd Code**\\n\\n```javascript\\n// In this example, our code accepts new Orders and while processing them approaches the Users Microservice\\ntest(\'When users service times out, then return 503 (option 1 with fake timers)\', async () => {\\n //Arrange\\n const clock = sinon.useFakeTimers();\\n config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls\\n nock(`${config.userServiceURL}/user/`)\\n .get(\'/1\', () => clock.tick(2000)) // Reply delay is bigger than configured timeout \ud83d\udc46\\n .reply(200);\\n const loggerDouble = sinon.stub(logger, \'error\');\\n const orderToAdd = {\\n userId: 1,\\n productId: 2,\\n mode: \'approved\',\\n };\\n\\n //Act\\n // \ud83d\udc47try to add new order which should fail due to User service not available\\n const response = await axiosAPIClient.post(\'/order\', orderToAdd);\\n\\n //Assert\\n // \ud83d\udc47At least our code does its best given this situation\\n expect(response.status).toBe(503);\\n expect(loggerDouble.lastCall.firstArg).toMatchObject({\\n name: \'user-service-not-available\',\\n stack: expect.any(String),\\n message: expect.any(String),\\n });\\n});\\n```\\n\\n## \ud83d\udc8a The \'poisoned message\' test - when the message consumer gets an invalid payload that might put it in stagnation\\n\\n**\ud83d\udc49What & so what -** When testing flows that start or end in a queue, I bet you\'re going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you\'re using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the \'poisoned message\'. To mitigate this risk, the tests\' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why\\n\\nWhen testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. [SQS demand 60 seconds](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-using-purge-queue.html) to purge queues), to name a few challenges that you won\'t find when dealing with real DB\\n\\nHere is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By \'fake\' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like [this one for SQS](https://github.com/m-radzikowski/aws-sdk-client-mock) and you can code one **easily** yourself. No worries, I\'m not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):\\n\\n**\ud83d\udcdd Code**\\n\\n1. Create a fake message queue that does almost nothing but record calls, see full example here\\n\\n```javascript\\nclass FakeMessageQueueProvider extends EventEmitter {\\n // Implement here\\n\\n publish(message) {}\\n\\n consume(queueName, callback) {}\\n}\\n```\\n\\n2. Make your message queue client accept real or fake provider\\n\\n```javascript\\nclass MessageQueueClient extends EventEmitter {\\n // Pass to it a fake or real message queue\\n constructor(customMessageQueueProvider) {}\\n\\n publish(message) {}\\n\\n consume(queueName, callback) {}\\n\\n // Simple implementation can be found here:\\n // https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js\\n}\\n```\\n\\n3. Expose a convenient function that tells when certain calls where made\\n\\n```javascript\\nclass MessageQueueClient extends EventEmitter {\\n publish(message) {}\\n\\n consume(queueName, callback) {}\\n\\n // \ud83d\udc47\\n waitForEvent(eventName: \'publish\' | \'consume\' | \'acknowledge\' | \'reject\', howManyTimes: number) : Promise\\n}\\n```\\n\\n4. The test is now short, flat and expressive \ud83d\udc47\\n\\n```javascript\\nconst FakeMessageQueueProvider = require(\'./libs/fake-message-queue-provider\');\\nconst MessageQueueClient = require(\'./libs/message-queue-client\');\\nconst newOrderService = require(\'./domain/newOrderService\');\\n\\ntest(\'When a poisoned message arrives, then it is being rejected back\', async () => {\\n // Arrange\\n const messageWithInvalidSchema = { nonExistingProperty: \'invalid\u274c\' };\\n const messageQueueClient = new MessageQueueClient(\\n new FakeMessageQueueProvider()\\n );\\n // Subscribe to new messages and passing the handler function\\n messageQueueClient.consume(\'orders.new\', newOrderService.addOrder);\\n\\n // Act\\n await messageQueueClient.publish(\'orders.new\', messageWithInvalidSchema);\\n // Now all the layers of the app will get stretched \ud83d\udc46, including logic and message queue libraries\\n\\n // Assert\\n await messageQueueClient.waitFor(\'reject\', { howManyTimes: 1 });\\n // \ud83d\udc46 This tells us that eventually our code asked the message queue client to reject this poisoned message\\n});\\n```\\n\\n**\ud83d\udcddFull code example -** [is here](https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/recipes/message-queue/fake-message-queue.test.js)\\n\\n## \ud83d\udce6 Test the package as a consumer\\n\\n**\ud83d\udc49What & why -** When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user\'s computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts _that were built_. See the mismatch here? _after_ running the tests, the package files are transpiled (I\'m looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files\\n\\n**\ud83d\udcdd Code**\\n\\nConsider the following scenario, you\'re developing a library, and you wrote this code:\\n```js\\n// index.js\\nexport * from \'./calculate.js\';\\n\\n// calculate.js \ud83d\udc48\\nexport function calculate() {\\n return 1;\\n}\\n```\\n\\nThen some tests:\\n```js\\nimport { calculate } from \'./index.js\';\\n\\ntest(\'should return 1\', () => {\\n expect(calculate()).toBe(1);\\n})\\n\\n\u2705 All tests pass \ud83c\udf8a\\n```\\n\\nFinally configure the package.json:\\n```json5\\n{\\n // ....\\n \\"files\\": [\\n \\"index.js\\"\\n ]\\n}\\n```\\n\\nSee, 100% coverage, all tests pass locally and in the CI \u2705, it just won\'t work in production \ud83d\udc79. Why? because you forgot to include the `calculate.js` in the package.json `files` array \ud83d\udc46\\n\\n\\nWhat can we do instead? we can test the library as _its end-users_. How? publish the package to a local registry like [verdaccio](https://verdaccio.org/), let the tests install and approach the *published* code. Sounds troublesome? judge yourself \ud83d\udc47\\n\\n**\ud83d\udcdd Code**\\n\\n```js\\n// global-setup.js\\n\\n// 1. Setup the in-memory NPM registry, one function that\'s it! \ud83d\udd25\\nawait setupVerdaccio();\\n\\n// 2. Building our package \\nawait exec(\'npm\', [\'run\', \'build\'], {\\n cwd: packagePath,\\n});\\n\\n// 3. Publish it to the in-memory registry\\nawait exec(\'npm\', [\'publish\', \'--registry=http://localhost:4873\'], {\\n cwd: packagePath,\\n});\\n\\n// 4. Installing it in the consumer directory\\nawait exec(\'npm\', [\'install\', \'my-package\', \'--registry=http://localhost:4873\'], {\\n cwd: consumerPath,\\n});\\n\\n// Test file in the consumerPath\\n\\n// 5. Test the package \ud83d\ude80\\ntest(\\"should succeed\\", async () => {\\n const { fn1 } = await import(\'my-package\');\\n\\n expect(fn1()).toEqual(1);\\n});\\n```\\n\\n**\ud83d\udcddFull code example -** [is here](https://github.com/rluvaton/e2e-verdaccio-example)\\n\\nWhat else this technique can be useful for?\\n\\n- Testing different version of peer dependency you support - let\'s say your package support react 16 to 18, you can now test that\\n- You want to test ESM and CJS consumers\\n- If you have CLI application you can test it like your users\\n- Making sure all the voodoo magic in that babel file is working as expected\\n\\n## \ud83d\uddde The \'broken contract\' test - when the code is great but its corresponding OpenAPI docs leads to a production bug\\n\\n**\ud83d\udc49What & so what -** Quite confidently I\'m sure that almost no team test their OpenAPI correctness. \\"It\'s just documentation\\", \\"we generate it automatically based on code\\" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.\\n\\nConsider the following scenario, you\'re requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don\'t forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the \'contract\' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., [PACT](https://pact.io)), there are also leaner approaches that gets you covered _easily and quickly_ (at the price of covering less risks).\\n\\nThe following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It\'s a pity that these libs can\'t assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:\\n\\n**\ud83d\udcdd Code**\\n\\n**Code under test, an API throw a new error status**\\n\\n```javascript\\nif (doesOrderCouponAlreadyExist) {\\n throw new AppError(\'duplicated-coupon\', { httpStatus: 409 });\\n}\\n```\\n\\nThe OpenAPI doesn\'t document HTTP status \'409\', no framework knows to update the OpenAPI doc based on thrown exceptions\\n\\n```json\\n\\"responses\\": {\\n \\"200\\": {\\n \\"description\\": \\"successful\\",\\n }\\n ,\\n \\"400\\": {\\n \\"description\\": \\"Invalid ID\\",\\n \\"content\\": {}\\n },// No 409 in this list\ud83d\ude32\ud83d\udc48\\n}\\n\\n```\\n\\n**The test code**\\n\\n```javascript\\nconst jestOpenAPI = require(\'jest-openapi\');\\njestOpenAPI(\'../openapi.json\');\\n\\ntest(\'When an order with duplicated coupon is added , then 409 error should get returned\', async () => {\\n // Arrange\\n const orderToAdd = {\\n userId: 1,\\n productId: 2,\\n couponId: uuid(),\\n };\\n await axiosAPIClient.post(\'/order\', orderToAdd);\\n\\n // Act\\n // We\'re adding the same coupon twice \ud83d\udc47\\n const receivedResponse = await axios.post(\'/order\', orderToAdd);\\n\\n // Assert;\\n expect(receivedResponse.status).toBe(409);\\n expect(res).toSatisfyApiSpec();\\n // This \ud83d\udc46 will throw if the API response, body or status, is different that was it stated in the OpenAPI\\n});\\n```\\n\\nTrick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in \'beforeAll\'. This covers all the tests against OpenAPI mismatches\\n\\n```javascript\\nbeforeAll(() => {\\n axios.interceptors.response.use((response) => {\\n expect(response.toSatisfyApiSpec());\\n // With this \ud83d\udc46, add nothing to the tests - each will fail if the response deviates from the docs\\n });\\n});\\n```\\n\\n## Even more ideas\\n\\n- Test readiness and health routes\\n- Test message queue connection failures\\n- Test JWT and JWKS failures\\n- Test security-related things like CSRF tokens\\n- Test your HTTP client retry mechanism (very easy with nock)\\n- Test that the DB migration succeed and the new code can work with old records format\\n- Test DB connection disconnects\\n \\n## It\'s not just ideas, it a whole new mindset\\n\\nThe examples above were not meant only to be a checklist of \'don\'t forget\' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this \'production-oriented development\'"},{"id":"monorepo-backend","metadata":{"permalink":"/blog/monorepo-backend","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/which-monorepo/index.md","source":"@site/blog/which-monorepo/index.md","title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","description":"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we\'d like to share our considerations in choosing our monorepo tooling","date":"2023-07-07T05:38:19.000Z","formattedDate":"July 7, 2023","tags":[{"label":"monorepo","permalink":"/blog/tags/monorepo"},{"label":"decisions","permalink":"/blog/tags/decisions"}],"readingTime":16.925,"hasTruncateMarker":true,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"},{"name":"Michael Salomon","title":"Practica.js core maintainer","url":"https://github.com/mikicho","imageURL":"https://avatars.githubusercontent.com/u/11459632?v=4","key":"michaelsalomon"}],"frontMatter":{"slug":"monorepo-backend","title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","authors":["goldbergyoni","michaelsalomon"],"tags":["monorepo","decisions"]},"prevItem":{"title":"Testing the dark scenarios of your Node.js application","permalink":"/blog/testing-the-dark-scenarios-of-your-nodejs-application"},"nextItem":{"title":"Practica v0.0.6 is alive","permalink":"/blog/practica-v0.0.6-is-alive"}},"content":"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in [Practica.js](https://github.com/practicajs/practica). In this post, we\'d like to share our considerations in choosing our monorepo tooling\\n\\n![Monorepos](./monorepo-high-level.png)\\n\\n## What are we looking\xa0at\\n\\n\\nThe Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries \u2014 [Lerna- has just retired.](https://github.com/lerna/lerna/issues/2703) When looking closely, it might not be just a coincidence \u2014 With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused \u2014 What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you\u2019re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow.\\n\\nThis post is concerned with backend-only and Node.js. It also scoped to _typical_ business solutions. If you\u2019re Google/FB developer who is faced with 8,000 packages \u2014 sorry, you need special gear. Consequently, monster Monorepo tooling like [Bazel](https://github.com/thundergolfer/example-bazel-monorepo) is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it\u2019s not actually maintained anymore \u2014 it\u2019s a good baseline for comparison).\\n\\nLet\u2019s start? When human beings use the term Monorepo, they typically refer to one or more of the following _4 layers below._ Each one of them can bring value to your project, each has different consequences, tooling, and features:\\n\\n\x3c!--truncate--\x3e\\n\\n\\n# Layer 1: Plain old folders to stay on top of your code\\n\\nWith zero tooling and only by having all the Microservice and libraries together in the same root folder, a developer gets great management perks and tons of value: Navigation, search across components, deleting a library instantly, debugging, _quickly_ adding new components. Consider the alternative with multi-repo approach \u2014 adding a new component for modularity demands opening and configuring a new GitHub repository. Not just a hassle but also greater chances of developers choosing the short path and including the new code in some semi-relevant existing package. In plain words, zero-tooling Monorepos can increase modularity.\\n\\nThis layer is often overlooked. If your codebase is not huge and the components are highly decoupled (more on this later)\u2014 it might be all you need. We\u2019ve seen a handful of successful Monorepo solutions without any special tooling.\\n\\nWith that said, some of the newer tools augment this experience with interesting features:\\n\\n- Both [Turborepo](https://turborepo.org/) and [Nx](https://nx.dev/structure/dependency-graph) and also [Lerna](https://www.npmjs.com/package/lerna-dependency-graph) provide a visual representation of the packages\u2019 dependencies\\n- [Nx allows \u2018visibility rules\u2019](https://nx.dev/structure/monorepo-tags) which is about enforcing who can use what. Consider, a \u2018checkout\u2019 library that should be approached only by the \u2018order Microservice\u2019 \u2014 deviating from this will result in failure during development (not runtime enforcement)\\n\\n![](https://miro.medium.com/max/1400/0*pHZKRlGT6iOKCmzg.jpg)\\n\\nNx dependencies graph\\n\\n- [Nx workspace generator](https://nx.dev/generators/workspace-generators) allows scaffolding out components. Whenever a team member needs to craft a new controller/library/class/Microservice, she just invokes a CLI command which products code based on a community or organization template. This enforces consistency and best practices sharing\\n\\n# Layer 2: Tasks and pipeline to build your code efficiently\\n\\nEven in a world of autonomous components, there are management tasks that must be applied in a batch like applying a security patch via npm update, running the tests of _multiple_ components that were affected by a change, publish 3 related libraries to name a few examples. All Monorepo tools support this basic functionality of invoking some command over a group of packages. For example, Lerna, Nx, and Turborepo do.\\n\\n![](https://miro.medium.com/max/1400/1*wu7xtN97-Ihz4uCSDwd0mA.png)\\n\\nApply some commands over multiple packages\\n\\nIn some projects, invoking a cascading command is all you need. Mostly if each package has an autonomous life cycle and the build process spans a single package (more on this later). In some other types of projects where the workflow demands testing/running and publishing/deploying many packages together \u2014 this will end in a terribly slow experience. Consider a solution with hundred of packages that are transpiled and bundled \u2014 one might wait minutes for a wide test to run. While it\u2019s not always a great practice to rely on wide/E2E tests, it\u2019s quite common in the wild. This is exactly where the new wave of Monorepo tooling shines \u2014 _deeply_ optimizing the build process. I should say this out loud: These tools bring beautiful and innovative build optimizations:\\n\\n- **Parallelization \u2014** If two commands or packages are orthogonal to each other, the commands will run in two different threads or processes. Typically your quality control involves testing, lining, license checking, CVE checking \u2014 why not parallelize?\\n- **Smart execution plan \u2014**Beyond parallelization, the optimized tasks execution order is determined based on many factors. Consider a build that includes A, B, C where A, C depend on B \u2014 naively, a build system would wait for B to build and only then run A & C. This can be optimized if we run A & C\u2019s _isolated_ unit tests _while_ building B and not afterward. By running task in parallel as early as possible, the overall execution time is improved \u2014 this has a remarkable impact mostly when hosting a high number of components. See below a visualization example of a pipeline improvement\\n\\n![](https://miro.medium.com/max/1400/0*C6cxCblQU8ckTIQk.png)\\n\\nA modern tool advantage over old Lerna. Taken from Turborepo website\\n\\n- **Detect who is affected by a change \u2014** Even on a system with high coupling between packages, it\u2019s usually not necessary to run _all_ packages rather than only those who are affected by a change. What exactly is \u2018affected\u2019? Packages/Microservices that depend upon another package that has changed. Some of the toolings can ignore minor changes that are unlikely to break others. This is not a great performance booster but also an amazing testing feature \u2014developers can get quick feedback on whether any of their clients were broken. Both Nx and Turborepo support this feature. Lerna can tell only which of the Monorepo package has changed\\n- **Sub-systems (i.e., projects) \u2014** Similarly to \u2018affected\u2019 above, modern tooling can realize portions of the graph that are inter-connected (a project or application) while others are not reachable by the component in context (another project) so they know to involve only packages of the relevant group\\n- **Caching \u2014** This is a serious speed booster: Nx and Turborepo cache the result/output of tasks and avoid running them again on consequent builds if unnecessary. For example, consider long-running tests of a Microservice, when commanding to re-build this Microservice, the tooling might realize that nothing has changed and the test will get skipped. This is achieved by generating a hashmap of all the dependent resources \u2014 if any of these resources haven\u2019t change, then the hashmap will be the same and the task will get skipped. They even cache the stdout of the command, so when you run a cached version it acts like the real thing \u2014 consider running 200 tests, seeing all the log statements of the tests, getting results over the terminal in 200 ms, everything acts like \u2018real testing while in fact, the tests did not run at all rather the cache!\\n- **Remote caching \u2014** Similarly to caching, only by placing the task\u2019s hashmaps and result on a global server so further executions on other team member\u2019s computers will also skip unnecessary tasks. In huge Monorepo projects that rely on E2E tests and must build all packages for development, this can save a great deal of time\\n\\n# Layer 3: Hoist your dependencies to boost npm installation\\n\\nThe speed optimizations that were described above won\u2019t be of help if the bottleneck is the big bull of mud that is called \u2018npm install\u2019 (not to criticize, it\u2019s just hard by nature). Take a typical scenario as an example, given dozens of components that should be built, they could easily trigger the installation of thousands of sub-dependencies. Although they use quite similar dependencies (e.g., same logger, same ORM), if the dependency version is not equal then npm will duplicate ([the NPM doppelgangers problem](https://rushjs.io/pages/advanced/npm_doppelgangers/)) the installation of those packages which might result in a long process.\\n\\nThis is where the workspace line of tools (e.g., Yarn workspace, npm workspaces, PNPM) kicks in and introduces some optimization \u2014 Instead of installing dependencies inside each component \u2018NODE_MODULES\u2019 folder, it will create one centralized folder and link all the dependencies over there. This can show a tremendous boost in install time for huge projects. On the other hand, if you always focus on one component at a time, installing the packages of a single Microservice/library should not be a concern.\\n\\nBoth Nx and Turborepo can rely on the package manager/workspace to provide this layer of optimizations. In other words, Nx and Turborepo are the layer above the package manager who take care of optimized dependencies installation.\\n\\n![](https://miro.medium.com/max/1400/1*dhyCWSbzpIi5iagR4OB4zQ.png)\\n\\nOn top of this, Nx introduces one more non-standard, maybe even controversial, technique: There might be only ONE package.json at the root folder of the entire Monorepo. By default, when creating components using Nx, they will not have their own package.json! Instead, all will share the root package.json. Going this way, all the Microservice/libraries share their dependencies and the installation time is improved. Note: It\u2019s possible to create \u2018publishable\u2019 components that do have a package.json, it\u2019s just not the default.\\n\\nI\u2019m concerned here. Sharing dependencies among packages increases the coupling, what if Microservice1 wishes to bump dependency1 version but Microservice2 can\u2019t do this at the moment? Also, package.json is part of Node.js _runtime_ and excluding it from the component root loses important features like package.json main field or ESM exports (telling the clients which files are exposed). I ran some POC with Nx last week and found myself blocked \u2014 library B was wadded, I tried to import it from Library A but couldn\u2019t get the \u2018import\u2019 statement to specify the right package name. The natural action was to open B\u2019s package.json and check the name, but there is no Package.json\u2026 How do I determine its name? Nx docs are great, finally, I found the answer, but I had to spend time learning a new \u2018framework\u2019.\\n\\n# Stop for a second: It\u2019s all about your workflow\\n\\nWe deal with tooling and features, but it\u2019s actually meaningless evaluating these options before determining whether your preferred workflow is _synchronized or independent_ (we will discuss this in a few seconds)_._ This upfront _fundamental_ decision will change almost everything.\\n\\nConsider the following example with 3 components: Library 1 is introducing some major and breaking changes, Microservice1 and Microservice2 depend upon Library1 and should react to those breaking changes. How?\\n\\n**Option A \u2014 The synchronized workflow-** Going with this development style, all the three components will be developed and deployed in one chunk _together_. Practically, a developer will code the changes in Library1, test libray1 and also run wide integration/e2e tests that include Microservice1 and Microservice2. When they\'re ready, the version of all components will get bumped. Finally, they will get deployed _together._\\n\\nGoing with this approach, the developer has the chance of seeing the full flow from the client\'s perspective (Microservice1 and 2), the tests cover not only the library but also through the eyes of the clients who actually use it. On the flip side, it mandates updating all the depend-upon components (could be dozens), doing so increases the risk\u2019s blast radius as more units are affected and should be considered before deployment. Also, working on a large unit of work demands building and testing more things which will slow the build.\\n\\n**Option B \u2014 Independent workflow-** This style is about working a unit by unit, one bite at a time, and deploy each component independently based on its personal business considerations and priority. This is how it goes: A developer makes the changes in Library1, they must be tested carefully in the scope of Library1. Once she is ready, the SemVer is bumped to a new major and the library is published to a package manager registry (e.g., npm). What about the client Microservices? Well, the team of Microservice2 is super-busy now with other priorities, and skip this update for now (the same thing as we all delay many of our npm updates,). However, Microservice1 is very much interested in this change \u2014 The team has to pro-actively update this dependency and grab the latest changes, run the tests and when they are ready, today or next week \u2014 deploy it.\\n\\nGoing with the independent workflow, the library author can move much faster because she does not need to take into account 2 or 30 other components \u2014 some are coded by different teams. This workflow also _forces her_ to write efficient tests against the library \u2014 it\u2019s her only safety net and is likely to end with autonomous components that have low coupling to others. On the other hand, testing in isolation without the client\u2019s perspective loses some dimension of realism. Also, if a single developer has to update 5 units \u2014 publishing each individually to the registry and then updating within all the dependencies can be a little tedious.\\n\\n![](https://miro.medium.com/max/1400/1*eeJFL3_vo5tCrWvVY-surg.png)\\n\\nSynchronized and independent workflows illustrated\\n\\n**On the illusion of synchronicity**\\n\\nIn distributed systems, it\u2019s not feasible to achieve 100% synchronicity \u2014 believing otherwise can lead to design faults. Consider a breaking change in Microservice1, now its client Microservice2 is adapting and ready for the change. These two Microservices are deployed together but due to the nature of Microservices and distributed runtime (e.g., Kubernetes) the deployment of Microservice1 only fail. Now, Microservice2\u2019s code is not aligned with Microservice1 production and we are faced with a production bug. This line of failures can be handled to an extent also with a synchronized workflow \u2014 The deployment should orchestrate the rollout of each unit so each one is deployed at a time. Although this approach is doable, it increased the chances of large-scoped rollback and increases deployment fear.\\n\\nThis fundamental decision, synchronized or independent, will determine so many things \u2014 Whether performance is an issue or not at all (when working on a single unit), hoisting dependencies or leaving a dedicated node_modules in every package\u2019s folder, and whether to create a local link between packages which is described in the next paragraph.\\n\\n# Layer 4: Link your packages for immediate feedback\\n\\nWhen having a Monorepo, there is always the unavoidable dilemma of how to link between the components:\\n\\n**Option 1: Using npm \u2014** Each library is a standard npm package and its client installs it via the standards npm commands. Given Microservice1 and Library1, this will end with two copies of Library1: the one inside Microservices1/NODE_MODULES (i.e., the local copy of the consuming Microservice), and the 2nd is the development folder where the team is coding Library1.\\n\\n**Option2: Just a plain folder \u2014** With this, Library1 is nothing but a logical module inside a folder that Microservice1,2,3 just locally imports. NPM is not involved here, it\u2019s just code in a dedicated folder. This is for example how Nest.js modules are represented.\\n\\nWith option 1, teams benefit from all the great merits of a package manager \u2014 SemVer(!), tooling, standards, etc. However, should one update Library1, the changes won\u2019t get reflected in Microservice1 since it is grabbing its copy from the npm registry and the changes were not published yet. This is a fundamental pain with Monorepo and package managers \u2014 one can\u2019t just code over multiple packages and test/run the changes.\\n\\nWith option 2, teams lose all the benefits of a package manager: Every change is propagated immediately to all of the consumers.\\n\\nHow do we bring the good from both worlds (presumably)? Using linking. Lerna, Nx, the various package manager workspaces (Yarn, npm, etc) allow using npm libraries and at the same time link between the clients (e.g., Microservice1) and the library. Under the hood, they created a symbolic link. In development mode, changes are propagated immediately, in deployment time \u2014 the copy is grabbed from the registry.\\n\\n![](https://miro.medium.com/max/1400/1*9PkNrnbnibFdbvPieq-y9g.png)\\n\\nLinking packages in a Monorepo\\n\\nIf you\u2019re doing the synchronized workflow, you\u2019re all set. Only now any risky change that is introduced by Library3, must be handled NOW by the 10 Microservices that consume it.\\n\\nIf favoring the independent workflow, this is of course a big concern. Some may call this direct linking style a \u2018monolith monorepo\u2019, or maybe a \u2018monolitho\u2019. However, when not linking, it\u2019s harder to debug a small issue between the Microservice and the npm library. What I typically do is _temporarily link_ (with npm link) between the packages_,_ debug, code, then finally remove the link.\\n\\nNx is taking a slightly more disruptive approach \u2014 it is using [TypeScript paths](https://www.typescriptlang.org/tsconfig#paths) to bind between the components. When Microservice1 is importing Library1, to avoid the full local path, it creates a TypeScript mapping between the library name and the full path. But wait a minute, there is no TypeScript in production so how could it work? Well, in serving/bundling time it webpacks and stitches the components together. Not a very standard way of doing Node.js work.\\n\\n# Closing: What should you use?\\n\\nIt\u2019s all about your workflow and architecture \u2014 a huge unseen cross-road stands in front of the Monorepo tooling decision.\\n\\n**Scenario A \u2014** If your architecture dictates a _synchronized workflow_ where all packages are deployed together, or at least developed in collaboration \u2014 then there is a strong need for a rich tool to manage this coupling and boost the performance. In this case, Nx might be a great choice.\\n\\nFor example, if your Microservice must keep the same versioning, or if the team really small and the same people are updating all the components, or if your modularization is not based on package manager but rather on framework-own modules (e.g., Nest.js), if you\u2019re doing frontend where the components inherently are published together, or if your testing strategy relies on E2E mostly \u2014 for all of these cases and others, Nx is a tool that was built to enhance the experience of coding many _relatively_ coupled components together. It is a great a sugar coat over systems that are unavoidably big and linked.\\n\\nIf your system is not inherently big or meant to synchronize packages deployment, fancy Monorepo features might increase the coupling between components. The Monorepo pyramid above draws a line between basic features that provide value without coupling components while other layers come with an architectural price to consider. Sometimes climbing up toward the tip is worth the consequences, just make this decision consciously.\\n\\n![](https://miro.medium.com/max/1400/1*c2qYYpVGG667bkum-gB-5Q.png)\\n\\n**Scenario B\u2014** If you\u2019re into an _independent workflow_ where each package is developed, tested, and deployed (almost) independently \u2014 then inherently there is no need to fancy tools to orchestrate hundreds of packages. Most of the time there is just one package in focus. This calls for picking a leaner and simpler tool \u2014 Turborepo. By going this route, Monorepo is not something that affects your architecture, but rather a scoped tool for faster build execution. One specific tool that encourages an independent workflow is [Bilt](https://github.com/giltayar/bilt) by Gil Tayar, it\u2019s yet to gain enough popularity but it might rise soon and is a great source to learn more about this philosophy of work.\\n\\n**In any scenario, consider workspaces \u2014** If you face performance issues that are caused by package installation, then the various workspace tools Yarn/npm/PNPM, can greatly minimize this overhead with a low footprint. That said, if you\u2019re working in an autonomous workflow, smaller are the chances of facing such issues. Don\u2019t just use tools unless there is a pain.\\n\\nWe tried to show the beauty of each and where it shines. If we\u2019re allowed to end this article with an opinionated choice: We greatly believe in an independent and autonomous workflow where the occasional developer of a package can code and deploy fearlessly without messing with dozens of other foreign packages. For this reason, Turborepo will be our favorite tool for the next season. We promise to tell you how it goes.\\n\\n# Bonus: Comparison table\\n\\nSee below a detailed comparison table of the various tools and features:\\n\\n![](https://miro.medium.com/max/1400/1*iHX_IdPW8XXXiZTyjFo6bw.png)\\n\\nPreview only, the complete table can be [found here](https://github.com/practicajs/practica/blob/main/docs/docs/decisions/monorepo.md)"},{"id":"practica-v0.0.6-is-alive","metadata":{"permalink":"/blog/practica-v0.0.6-is-alive","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/v0.6-is-alive/index.md","source":"@site/blog/v0.6-is-alive/index.md","title":"Practica v0.0.6 is alive","description":"Where is our focus now?","date":"2022-12-10T10:00:00.000Z","formattedDate":"December 10, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"practica","permalink":"/blog/tags/practica"},{"label":"prisma","permalink":"/blog/tags/prisma"}],"readingTime":1.47,"hasTruncateMarker":false,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"},{"name":"Raz Luvaton","title":"Practica.js core maintainer","url":"https://github.com/rluvaton","imageURL":"https://avatars.githubusercontent.com/u/16746759?v=4","key":"razluvaton"},{"name":"Daniel Gluskin","title":"Practica.js core maintainer","url":"https://github.com/DanielGluskin","imageURL":"https://avatars.githubusercontent.com/u/17989958?v=4","key":"danielgluskin"},{"name":"Michael Salomon","title":"Practica.js core maintainer","url":"https://github.com/mikicho","imageURL":"https://avatars.githubusercontent.com/u/11459632?v=4","key":"michaelsalomon"}],"frontMatter":{"slug":"practica-v0.0.6-is-alive","date":"2022-12-10T10:00","hide_table_of_contents":true,"title":"Practica v0.0.6 is alive","authors":["goldbergyoni","razluvaton","danielgluskin","michaelsalomon"],"tags":["node.js","express","practica","prisma"]},"prevItem":{"title":"Which Monorepo is right for a Node.js BACKEND\xa0now?","permalink":"/blog/monorepo-backend"},"nextItem":{"title":"Is Prisma better than your \'traditional\' ORM?","permalink":"/blog/is-prisma-better-than-your-traditional-orm"}},"content":"## Where is our focus now?\\n\\nWe work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback\\n\\n## What\'s new?\\n\\n### Request-level store\\n\\nEvery request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is \'request-id\' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in [AsyncLocal](https://nodejs.org/api/async_context.html) for this task\\n\\n### Hardened .dockerfile\\n\\nAlthough a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from [this article](https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/) and already apply 90% of the guidelines\\n\\n### Additional ORM option: Prisma\\n\\nPrisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma\\n\\nWhy did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this [blog post](https://practica.dev/blog/is-prisma-better-than-your-traditional-orm/)\\n\\n### Many small enhancements\\n\\nMore than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more\\n\\n## Where do I start?\\n\\nDefinitely follow the [getting started guide first](https://practica.dev/the-basics/getting-started-quickly) and then read the guide [coding with practica](https://practica.dev/the-basics/coding-with-practica) to realize its full power and genuine value. We will be thankful to receive your feedback"},{"id":"is-prisma-better-than-your-traditional-orm","metadata":{"permalink":"/blog/is-prisma-better-than-your-traditional-orm","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/is-prisma-better/index.md","source":"@site/blog/is-prisma-better/index.md","title":"Is Prisma better than your \'traditional\' ORM?","description":"Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?","date":"2022-12-07T11:00:00.000Z","formattedDate":"December 7, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"nestjs","permalink":"/blog/tags/nestjs"},{"label":"fastify","permalink":"/blog/tags/fastify"},{"label":"passport","permalink":"/blog/tags/passport"},{"label":"dotenv","permalink":"/blog/tags/dotenv"},{"label":"supertest","permalink":"/blog/tags/supertest"},{"label":"practica","permalink":"/blog/tags/practica"},{"label":"testing","permalink":"/blog/tags/testing"}],"readingTime":23.875,"hasTruncateMarker":true,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"}],"frontMatter":{"slug":"is-prisma-better-than-your-traditional-orm","date":"2022-12-07T11:00","hide_table_of_contents":true,"title":"Is Prisma better than your \'traditional\' ORM?","authors":["goldbergyoni"],"tags":["node.js","express","nestjs","fastify","passport","dotenv","supertest","practica","testing"]},"prevItem":{"title":"Practica v0.0.6 is alive","permalink":"/blog/practica-v0.0.6-is-alive"},"nextItem":{"title":"Popular Node.js patterns and tools to re-consider","permalink":"/blog/popular-nodejs-pattern-and-tools-to-reconsider"}},"content":"## Intro - Why discuss yet another ORM (or the man who had a stain on his fancy suite)?\\n\\n*Betteridge\'s law of headlines suggests that a \'headline that ends in a question mark can be answered by the word NO\'. Will this article follow this rule?*\\n\\nImagine an elegant businessman (or woman) walking into a building, wearing a fancy tuxedo and a luxury watch wrapped around his palm. He smiles and waves all over to say hello while people around are starring admirably. You get a little closer, then shockingly, while standing nearby it\'s hard ignore a bold a dark stain over his white shirt. What a dissonance, suddenly all of that glamour is stained\\n\\n![Suite with stain](./suite.png)\\n\\n Like this businessman, Node is highly capable and popular, and yet, in certain areas, its offering basket is stained with inferior offerings. One of these areas is the ORM space, \\"I wish we had something like (Java) hibernate or (.NET) Entity Framework\\" are common words being heard by Node developers. What about existing mature ORMs like TypeORM and Sequelize? We owe so much to these maintainers, and yet, the produced developer experience, the level of maintenance - just don\'t feel delightful, some may say even mediocre. At least so I believed *before* writing this article...\\n\\nFrom time to time, a shiny new ORM is launched, and there is hope. Then soon it\'s realized that these new emerging projects are more of the same, if they survive. Until one day, Prisma ORM arrived surrounded with glamour: It\'s gaining tons of attention all over, producing fantastic content, being used by respectful frameworks and... raised 40,000,000$ (40 million) to build the next generation ORM - Is it the \'Ferrari\' ORM we\'ve been waiting for? Is it a game changer? If you\'re are the \'no ORM for me\' type, will this one make you convert your religion?\\n\\nIn [Practica.js](https://github.com/practicajs/practica) (the Node.js starter based off [Node.js best practices with 83,000 stars](https://github.com/goldbergyoni/nodebestpractices)) we aim to make the best decisions for our users, the Prisma hype made us stop by for a second, evaluate its unique offering and conclude whether we should upgrade our toolbox?\\n\\nThis article is certainly not an \'ORM 101\' but rather a spotlight on specific dimensions in which Prisma aims to shine or struggle. It\'s compared against the two most popular Node.js ORM - TypeORM and Sequelize. Why not others? Why other promising contenders like MikroORM weren\'t covered? Just because they are not as popular yet ana maturity is a critical trait of ORMs\\n\\nReady to explore how good Prisma is and whether you should throw away your current tools?\\n\\n\x3c!--truncate--\x3e\\n\\n## TOC\\n\\n1. Prisma basics in 3 minutes\\n2. Things that are mostly the same\\n3. Differentiation\\n4. Closing\\n\\n## Prisma basics in 3 minutes\\n\\nJust before delving into the strategic differences, for the benefit of those unfamiliar with Prisma - here is a quick \'hello-world\' workflow with Prisma ORM. If you\'re already familiar with it - skipping to the next section sounds sensible. Simply put, Prisma dictates 3 key steps to get our ORM code working:\\n\\n**A. Define a model -** Unlike almost any other ORM, Prisma brings a unique language (DSL) for modeling the database-to-code mapping. This proprietary syntax aims to express these models with minimum clutter (i.e., TypeScript generics and verbose code). Worried about having intellisense and validation? A well-crafted vscode extension gets you covered. In the following example, the prisma.schema file describes a DB with an Order table that has a one-to-many relation with a Country table:\\n\\n```prisma\\n// prisma.schema file\\nmodel Order {\\n id Int @id @default(autoincrement())\\n userId Int?\\n paymentTermsInDays Int?\\n deliveryAddress String? @db.VarChar(255)\\n country Country @relation(fields: [countryId], references: [id])\\n countryId Int\\n}\\n\\nmodel Country {\\n id Int @id @default(autoincrement())\\n name String @db.VarChar(255)\\n Order Order[]\\n}\\n```\\n\\n**B. Generate the client code -** Another unusual technique: to get the ORM code ready, one must invoke Prisma\'s CLI and ask for it: \\n\\n```bash\\nnpx prisma generate\\n```\\n\\nAlternatively, if you wish to have your DB ready and the code generated with one command, just fire:\\n\\n```bash\\nnpx prisma migrate deploy\\n```\\n\\nThis will generate migration files that you can execute later in production and also the ORM client code\\n\\n\\nThis will generate migration files that you can execute later in production and the TypeScript ORM code based on the model. The generated code location is defaulted under \'[root]/NODE_MODULES/.prisma/client\'. Every time the model changes, the code must get re-generated again. While most ORMs name this code \'repository\' or \'entity\' or \'active record\', interestingly, Prisma calls it a \'client\'. This shows part of its unique philosophy, which we will explore later\\n\\n**C. All good, use the client to interact with the DB -** The generated client has a rich set of functions and types for your DB interactions. Just import the ORM/client code and use it:\\n\\n```javascript\\nimport { PrismaClient } from \'.prisma/client\';\\n\\nconst prisma = new PrismaClient();\\n// A query example\\nawait prisma.order.findMany({\\n where: {\\n paymentTermsInDays: 30,\\n },\\n orderBy: {\\n id: \'asc\',\\n },\\n });\\n// Use the same client for insertion, deletion, updates, etc\\n```\\n\\nThat\'s the nuts and bolts of Prisma. Is it different and better?\\n\\n## What is the same?\\n\\nWhen comparing options, before outlining differences, it\'s useful to state what is actually similar among these products. Here is a partial list of features that both TypeORM, Sequelize and Prisma support\\n\\n- Casual queries with sorting, filtering, distinct, group by, \'upsert\' (update or create),etc\\n- Raw queries\\n- Full text search\\n- Association/relations of any type (e.g., many to many, self-relation, etc)\\n- Aggregation queries\\n- Pagination\\n- CLI\\n- Transactions\\n- Migration & seeding\\n- Hooks/events (called middleware in Prisma)\\n- Connection pool\\n- Based on various community benchmarks, no dramatic performance differences\\n- All have huge amount of stars and downloads\\n\\nOverall, I found TypeORM and Sequelize to be a little more feature rich. For example, the following features are not supported only in Prisma: GIS queries, DB-level custom constraints, DB replication, soft delete, caching, exclude queries and some more\\n\\nWith that, shall we focus on what really set them apart and make a difference\\n\\n## What is fundamentally different?\\n\\n### 1. Type safety across the board\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** ORM\'s life is not easier since the TypeScript rise, to say the least. The need to support typed models/queries/etc yields a lot of developers sweat. Sequelize, for example, struggles to stabilize a TypeScript interface and, by now offers 3 different syntaxes + one external library ([sequelize-typescript](https://github.com/sequelize/sequelize-typescript)) that offers yet another style. Look at the syntax below, this feels like an afterthought - a library that was not built for TypeScript and now tries to squeeze it in somehow. Despite the major investment, both Sequelize and TypeORM offer only partial type safety. Simple queries do return typed objects, but other common corner cases like attributes/projections leave you with brittle strings. Here are a few examples:\\n\\n\\n```javascript\\n// Sequelize pesky TypeScript interface\\ntype OrderAttributes = {\\n id: number,\\n price: number,\\n // other attributes...\\n};\\n\\ntype OrderCreationAttributes = Optional;\\n\\n//\ud83d\ude2f Isn\'t this a weird syntax?\\nclass Order extends Model, InferCreationAttributes> {\\n declare id: CreationOptional;\\n declare price: number;\\n}\\n```\\n\\n```javascript\\n// Sequelize loose query types\\nawait getOrderModel().findAll({\\n where: { noneExistingField: \'noneExistingValue\' } //\ud83d\udc4d TypeScript will warn here\\n attributes: [\'none-existing-field\', \'another-imaginary-column\'], // No errors here although these columns do not exist\\n include: \'no-such-table\', //\ud83d\ude2f no errors here although this table doesn\'t exist\\n });\\n await getCountryModel().findByPk(\'price\'); //\ud83d\ude2f No errors here although the price column is not a primary key\\n```\\n\\n```javascript\\n// TypeORM loose query\\nconst ordersOnSales: Post[] = await orderRepository.find({\\n where: { onSale: true }, //\ud83d\udc4d TypeScript will warn here\\n select: [\'id\', \'price\'],\\n})\\nconsole.log(ordersOnSales[0].userId); //\ud83d\ude2f No errors here although the \'userId\' column is not part of the returned object\\n```\\n\\nIsn\'t it ironic that a library called **Type**ORM base its queries on strings?\\n\\n\\n**\ud83e\udd14 How Prisma is different:** It takes a totally different approach by generating per-project client code that is fully typed. This client embodies types for everything: every query, relations, sub-queries, everything (except migrations). While other ORMs struggles to infer types from discrete models (including associations that are declared in other files), Prisma\'s offline code generation is easier: It can look through the entire DB relations, use custom generation code and build an almost perfect TypeScript experience. Why \'almost\' perfect? for some reason, Prisma advocates using plain SQL for migrations, which might result in a discrepancy between the code models and the DB schema. Other than that, this is how Prisma\'s client brings end to end type safety:\\n\\n```javascript\\nawait prisma.order.findMany({\\n where: {\\n noneExistingField: 1, //\ud83d\udc4d TypeScript error here\\n },\\n select: {\\n noneExistingRelation: { //\ud83d\udc4d TypeScript error here\\n select: { id: true }, \\n },\\n noneExistingField: true, //\ud83d\udc4d TypeScript error here\\n },\\n });\\n\\n await prisma.order.findUnique({\\n where: { price: 50 }, //\ud83d\udc4d TypeScript error here\\n });\\n```\\n\\n**\ud83d\udcca How important:** TypeScript support across the board is valuable for DX mostly. Luckily, we have another safety net: The project testing. Since tests are mandatory, having build-time type verification is important but not a life saver\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Definitely\\n\\n## 2. Make you forget SQL\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Many avoid ORMs while preferring to interact with the DB using lower-level techniques. One of their arguments is against the efficiency of ORMs: Since the generated queries are not visible immediately to the developers, wasteful queries might get executed unknowingly. While all ORMs provide syntactic sugar over SQL, there are subtle differences in the level of abstraction. The more the ORM syntax resembles SQL, the more likely the developers will understand their own actions\\n\\nFor example, TypeORM\'s query builder looks like SQL broken into convenient functions\\n\\n```javascript\\nawait createQueryBuilder(\'order\')\\n .leftJoinAndSelect(\\n \'order.userId\',\\n \'order.productId\',\\n \'country.name\',\\n \'country.id\'\\n )\\n .getMany();\\n```\\n\\nA developer who read this code \ud83d\udc46 is likely to infer that a *join* query between two tables will get executed\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Prisma\'s mission statement is to simplify DB work, the following statement is taken from their homepage:\\n\\n\\"We designed its API to be intuitive, both for SQL veterans and *developers brand new to databases*\\"\\n\\nBeing ambitious to appeal also to database layman, Prisma builds a syntax with a little bit higher abstraction, for example:\\n\\n```javascript\\nawait prisma.order.findMany({\\n select: {\\n userId: true,\\n productId: true,\\n country: {\\n select: { name: true, id: true },\\n },\\n },\\n});\\n\\n```\\n\\nNo join is reminded here also it fetches records from two related tables (order, and country). Could you guess what SQL is being produced here? how many queries? One right, a simple join? Surprise, actually, two queries are made. Prisma fires one query per-table here, as the join logic happens on the ORM client side (not inside the DB). But why?? in some cases, mostly where there is a lot of repetition in the DB cartesian join, querying each side of the relation is more efficient. But in other cases, it\'s not. Prisma arbitrarily chose what they believe will perform better in *most* cases. I checked, in my case it\'s *slower* than doing a one-join query on the DB side. As a developer, I would miss this deficiency due to the high-level syntax (no join is mentioned). My point is, Prisma sweet and simple syntax might be a bless for developer who are brand new to databases and aim to achieve a working solution in a short time. For the longer term, having full awareness of the DB interactions is helpful, other ORMs encourage this awareness a little better\\n\\n**\ud83d\udcca How important:** Any ORM will hide SQL details from their users - without developer\'s awareness no ORM will save the day\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Not necessarily\\n\\n## 3. Performance\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Speak to an ORM antagonist and you\'ll hear a common sensible argument: ORMs are much slower than a \'raw\' approach. To an extent, this is a legit observation as [most comparisons](https://welingtonfidelis.medium.com/pg-driver-vs-knex-js-vs-sequelize-vs-typeorm-f9ed53e9f802) will show none-negligible differences between raw/query-builder and ORM.\\n\\n![raw is faster d](./pg-driver-is-faster.png)\\n*Example: a direct insert against the PG driver is much shorter [Source](https://welingtonfidelis.medium.com/pg-driver-vs-knex-js-vs-sequelize-vs-typeorm-f9ed53e9f802)* \\n\\n It should also be noted that these benchmarks don\'t tell the entire story - on top of raw queries, every solution must build a mapper layer that maps the raw data to JS objects, nest the results, cast types, and more. This work is included within every ORM but not shown in benchmarks for the raw option. In reality, every team which doesn\'t use ORM would have to build their own small \\"ORM\\", including a mapper, which will also impact performance\\n\\n\\n**\ud83e\udd14 How Prisma is different:** It was my hope to see a magic here, eating the ORM cake without counting the calories, seeing Prisma achieving an almost \'raw\' query speed. I had some good and logical reasons for this hope: Prisma uses a DB client built with Rust. Theoretically, it could serialize to and nest objects faster (in reality, this happens on the JS side). It was also built from the ground up and could build on the knowledge pilled in ORM space for years. Also, since it returns POJOs only (see bullet \'No Active Record here!\') - no time should be spent on decorating objects with ORM fields\\n\\nYou already got it, this hope was not fulfilled. Going with every community benchmark ([one](https://dev.to/josethz00/benchmark-prisma-vs-typeorm-3873), [two](https://github.com/edgedb/imdbench), [three](https://deepkit.io/library)), Prisma at best is not faster than the average ORM. What is the reason? I can\'t tell exactly but it might be due the complicated system that must support Go, future languages, MongoDB and other non-relational DBs\\n\\n![Prisma is not faster](./throughput-benchmark.png)\\n*Example: Prisma is not faster than others. It should be noted that in other benchmarks Prisma scores higher and shows an \'average\' performance [Source](https://github.com/edgedb/imdbench)*\\n\\n**\ud83d\udcca How important:** It\'s expected from ORM users to live peacefully with inferior performance, for many systems it won\'t make a great deal. With that, 10%-30% performance differences between various ORMs are not a key factor\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** No\\n\\n## 4. No active records here!\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Node in its early days was heavily inspired by Ruby (e.g., testing \\"describe\\"), many great patterns were embraced, [Active Record](https://en.wikipedia.org/wiki/Active_record_pattern) is not among the successful ones. What is this pattern about in a nutshell? say you deal with Orders in your system, with Active Record an Order object/class will hold both the entity properties, possible also some of the logic functions and also CRUD functions. Many find this pattern to be awful, why? ideally, when coding some logic/flow, one should not keep her mind busy with side effects and DB narratives. It also might be that accessing some property unconsciously invokes a heavy DB call (i.e., lazy loading). If not enough, in case of heavy logic, unit tests might be in order (i.e., read [\'selective unit tests\'](https://blog.stevensanderson.com/2009/11/04/selective-unit-testing-costs-and-benefits/)) - it\'s going to be much harder to write unit tests against code that interacts with the DB. In fact, all of the respectable and popular architecture (e.g., DDD, clean, 3-tiers, etc) advocate to \'isolate the domain\', separate the core/logic of the system from the surrounding technologies. With all of that said, both TypeORM and Sequelize support the Active Record pattern which is displayed in many examples within their documentation. Both also support other better patterns like the data mapper (see below), but they still open the door for doubtful patterns\\n\\n\\n```javascript\\n// TypeORM active records \ud83d\ude1f\\n\\n@Entity()\\nclass Order extends BaseEntity {\\n @PrimaryGeneratedColumn()\\n id: number\\n\\n @Column()\\n price: number\\n\\n @ManyToOne(() => Product, (product) => product.order)\\n products: Product[]\\n\\n // Other columns here\\n}\\n\\nfunction updateOrder(orderToUpdate: Order){\\n if(orderToUpdate.price > 100){\\n // some logic here\\n orderToUpdate.status = \\"approval\\";\\n orderToUpdate.save(); \\n orderToUpdate.products.forEach((products) =>{ \\n\\n })\\n orderToUpdate.usedConnection = ? \\n }\\n}\\n\\n\\n\\n```\\n\\n**\ud83e\udd14 How Prisma is different:** The better alternative is the data mapper pattern. It acts as a bridge, an adapter, between simple object notations (domain objects with properties) to the DB language, typically SQL. Call it with a plain JS object, POJO, get it saved in the DB. Simple. It won\'t add functions to the result objects or do anything beyond returning pure data, no surprising side effects. In its purest sense, this is a DB-related utility and completely detached from the business logic. While both Sequelize and TypeORM support this, Prisma offers *only* this style - no room for mistakes.\\n\\n\\n```javascript\\n// Prisma approach with a data mapper \ud83d\udc4d\\n\\n// This was generated automatically by Prisma\\ntype Order {\\n id: number\\n\\n price: number\\n\\n products: Product[]\\n\\n // Other columns here\\n}\\n\\nfunction updateOrder(orderToUpdate: Order){\\n if(orderToUpdate.price > 100){\\n orderToUpdate.status = \\"approval\\";\\n prisma.order.update({ where: { id: orderToUpdate.id }, data: orderToUpdate }); \\n // Side effect \ud83d\udc46, but an explicit one. The thoughtful coder will move this to another function. Since it\'s happening outside, mocking is possible \ud83d\udc4d\\n products.forEach((products) =>{ // No lazy loading, the data is already here \ud83d\udc4d\\n\\n })\\n } \\n}\\n```\\n\\n In [Practica.js](https://github.com/practicajs/practica) we take it one step further and put the prisma models within the \\"DAL\\" layer and wrap it with the [repository pattern](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-design). You may glimpse [into the code here](https://github.com/practicajs/practica/blob/21ff12ba19cceed9a3735c09d48184b5beb5c410/src/code-templates/services/order-service/domain/new-order-use-case.ts#L21), this is the business flow that calls the DAL layer\\n\\n\\n**\ud83d\udcca How important:** On the one hand, this is a key architectural principle to follow but the other hand most ORMs *allow* doing it right\\n\\n![Medium importance](./high1-importance-slider.png)\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Yes!\\n\\n## 5. Documentation and developer-experience\\n\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** TypeORM and Sequelize documentation is mediocre, though TypeORM is a little better. Based on my personal experience they do get a little better over the years, but still by no mean they deserve to be called \\"good\\" or \\"great\\". For example, if you seek to learn about \'raw queries\' - Sequelize offers [a very short page](https://sequelize.org/docs/v6/core-concepts/raw-queries/) on this matter, TypeORM info is spread in multiple other pages. Looking to learn about pagination? Couldn\'t find Sequelize documents, TypeORM has [some short explanation](https://typeorm.io/select-query-builder#using-pagination), 150 words only\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Prisma documentation rocks! See their documents on similar topics: [raw queries](https://www.prisma.io/docs/concepts/components/prisma-client/raw-database-access) and [pagingation](https://www.prisma.io/docs/concepts/components/prisma-client/pagination), thousands of words, and dozens of code examples. The writing itself is also great, feels like some professional writers were involved\\n\\n![Prisma docs are comprehensive](./count-docs.png)\\n \\nThis chart above shows how comprehensive are Prisma docs (Obviously this by itself doesn\'t prove quality)\\n\\n**\ud83d\udcca How important:** Great docs are a key to awareness and avoiding pitfalls\\n\\n![Medium importance](./high1-importance-slider.png)\\n\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** You bet\\n\\n## 6. Observability, metrics, and tracing\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Good chances are (say about 99.9%) that you\'ll find yourself diagnostic slow queries in production or any other DB-related quirks. What can you expect from traditional ORMs in terms of observability? Mostly logging. [Sequelize provides both logging](https://sequelize.org/api/v7/interfaces/queryoptions#benchmark) of query duration and programmatic access to the connection pool state ({size,available,using,waiting}). [TypeORM provides only logging](https://orkhan.gitbook.io/typeorm/docs/logging) of queries that suppress a pre-defined duration threshold. This is better than nothing, but assuming you don\'t read production logs 24/7, you\'d probably need more than logging - an alert to fire when things seem faulty. To achieve this, it\'s your responsibility to bridge this info to your preferred monitoring system. Another logging downside for this sake is verbosity - we need to emit tons of information to the logs when all we really care for is the average duration. Metrics can serve this purpose much better as we\'re about to see soon with Prisma\\n\\nWhat if you need to dig into which specific part of the query is slow? unfortunately, there is no breakdown of the query phases duration - it\'s being left to you as a black-box\\n\\n```javascript\\n// Sequelize - logging various DB information\\n\\n```\\n\\n![Logging query duration](./sequelize-log.png)\\nLogging each query in order to realize trends and anomaly in the monitoring system\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Since Prisma targets also enterprises, it must bring strong ops capabilities. Beautifully, it packs support for both [metrics](https://www.prisma.io/docs/concepts/components/prisma-client/metrics) and [open telemetry tracing](https://www.prisma.io/docs/concepts/components/prisma-client/opentelemetry-tracing)!. For metrics, it generates custom JSON with metric keys and values so anyone can adapt this to any monitoring system (e.g., CloudWatch, statsD, etc). On top of this, it produces out of the box metrics in [Prometheus](https://prometheus.io/) format (one of the most popular monitoring platforms). For example, the metric \'prisma_client_queries_duration_histogram_ms\' provides the average query length in the system overtime. What is even more impressive is the support for open-tracing - it feeds your OpenTelemetry collector with spans that describe the various phases of every query. For example, it might help realize what is the bottleneck in the query pipeline: Is it the DB connection, the query itself or the serialization?\\n\\n![prisma tracing](./trace-diagram.png)\\nPrisma visualizes the various query phases duration with open-telemtry \\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Definitely\\n\\n\\n**\ud83d\udcca How important:** Goes without words how impactful is observability, however filling the gap in other ORM will demand no more than a few days\\n\\n![Medium importance](./medium2-importance-slider.png)\\n\\n## 7. Continuity - will it be here with us in 2024/2025\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** We live quite peacefully with the risk of one of our dependencies to disappear. With ORM though, this risk demand special attention because our buy-in is higher (i.e., harder to replace) and maintaining it was proven to be harder. Just look at a handful of successful ORMs in the past: objection.js, waterline, bookshelf - all of these respectful project had 0 commits in the past month. The single maintainer of objection.js [announced that he won\'t work the project anymore](https://github.com/Vincit/objection.js/issues/2335). This high churn rate is not surprising given the huge amount of moving parts to maintain, the gazillion corner cases and the modest \'budget\' OSS projects live with. Looking at OpenCollective shows that [Sequelize](https://opencollective.com/sequelize#category-BUDGET) and [TypeORM](https://opencollective.com/typeorm) are funded with ~1500$ month in average. This is barely enough to cover a daily Starbucks cappuccino and croissant (6.95$ x 365) for 5 maintainers. Nothing contrasts this model more than a startup company that just raised its series B - Prisma is [funded with 40,000,000$ (40 millions)](https://www.prisma.io/blog/series-b-announcement-v8t12ksi6x#:~:text=We%20are%20excited%20to%20announce,teams%20%26%20organizations%20in%20this%20article.) and recruited 80 people! Should not this inspire us with high confidence about their continuity? I\'ll surprisingly suggest that quite the opposite is true\\n\\nSee, an OSS ORM has to go over one huge hump, but a startup company must pass through TWO. The OSS project will struggle to achieve the critical mass of features, including some high technical barriers (e.g., TypeScript support, ESM). This typically lasts years, but once it does - a project can focus mostly on maintenance and step out of the danger zone. The good news for TypeORM and Sequelize is that they already did! Both struggled to keep their heads above the water, there were rumors in the past that [TypeORM is not maintained anymore](https://github.com/typeorm/typeorm/issues/3267), but they managed to go through this hump. I counted, both projects had approximately ~2000 PRs in the past 3 years! Going with [repo-tracker](https://repo-tracker.com/r/gh/sequelize/sequelize), each see multiple commits every week. They both have vibrant traction, and the majority of features you would expect from an ORM. TypeORM even supports beyond-the-basics features like multi data source and caching. It\'s unlikely that now, once they reached the promise land - they will fade away. It might happen, there is no guarantee in the OSS galaxy, but the risk is low\\n\\n![One hump](./one-hump.png)\\n\\n\\n**\ud83e\udd14 How Prisma is different:** Prisma a little lags behind in terms of features, but with a budget of 40M$ - there are good reasons to believe that they will pass the first hump, achieving a critical mass of features. I\'m more concerned with the second hump - showing revenues in 2 years or saying goodbye. As a company that is backed by venture capitals - the model is clear and cruel: In order to secure their next round, series B or C (depends whether the seed is counted), there must be a viable and proven business model. How do you \'sell\' ORM? Prisma experiments with multiple products, none is mature yet or being paid for. How big is this risk? According to [this startup companies success statistics](https://spdload.com/blog/startup-success-rate/), \\"About 65% of the Series A startups get series B, while 35% of the companies that get series A fail.\\". Since Prisma already gained a lot of love and adoption from the community, there success chances are higher than the average round A/B company, but even 20% or 10% chances to fade away is concerning\\n\\n> This is terrifying news - companies happily choose a young commercial OSS product without realizing that there are 10-30% chances for this product to disappear\\n\\n\\n![Two humps](./two-humps.png)\\n\\nSome of startup companies who seek a viable business model do not shut the doors rather change the product, the license or the free features. This is not my subjective business analysis, here are few examples: [MongoDB changed their license](https://techcrunch.com/2018/10/16/mongodb-switches-up-its-open-source-license/), this is why the majority had to host their Mongo DB over a single vendor. [Redis did something similar](https://techcrunch.com/2019/02/21/redis-labs-changes-its-open-source-license-again/). What are the chances of Prisma pivoting to another type of product? It actually already happened before, Prisma 1 was mostly about graphQL client and server, [it\'s now retired](https://github.com/prisma/prisma1)\\n\\nIt\'s just fair to mention the other potential path - most round B companies do succeed to qualify for the next round, when this happens even bigger money will be involved in building the \'Ferrari\' of JavaScript ORMs. I\'m surely crossing my fingers for these great people, at the same time we have to be conscious about our choices\\n\\n**\ud83d\udcca How important:** As important as having to code again the entire DB layer in a big system\\n\\n![Medium importance](./high2-importance-slider.png)\\n\\n\\n**\ud83c\udfc6 Is Prisma doing better?:** Quite the opposite\\n\\n## Closing - what should you use now?\\n\\nBefore proposing my key take away - which is the primary ORM, let\'s repeat the key learning that were introduced here:\\n\\n1. \ud83e\udd47 Prisma deserves a medal for its awesome DX, documentation, observability support and end-to-end TypeScript coverage\\n2. \ud83e\udd14 There are reasons to be concerned about Prisma\'s business continuity as a young startup without a viable business model. Also Prisma\'s abstract client syntax might blind developers a little more than other ORMs\\n3. \ud83c\udfa9 The contenders, TypeORM and Sequelize, matured and doing quite well: both have merged thousand PRs in the past 3 years to become more stable, they keep introducing new releases (see [repo-tracker](https://repo-tracker.com/r/gh/sequelize/sequelize)), and for now holds more features than Prisma. Also, both show solid performance (for an ORM). Hats off to the maintainers!\\n\\nBased on these observations, which should you pick? which ORM will we use for [practica.js](https://github.com/practicajs/practica)?\\n \\nPrisma is an excellent addition to Node.js ORMs family, but not the hassle-free one tool to rule them all. It\'s a mixed bag of many delicious candies and a few gotchas. Wouldn\'t it grow to tick all the boxes? Maybe, but unlikely. Once built, it\'s too hard to dramatically change the syntax and engine performance. Then, during the writing and speaking with the community, including some Prisma enthusiasts, I realized that it doesn\'t aim to be the can-do-everything \'Ferrari\'. Its positioning seems to resemble more a convenient family car with a solid engine and awesome user experience. In other words, it probably aims for the enterprise space where there is mostly demand for great DX, OK performance, and business-class support\\n\\nIn the end of this journey I see no dominant flawless \'Ferrari\' ORM. I should probably change my perspective: Building ORM for the hectic modern JavaScript ecosystem is 10x harder than building a Java ORM back then in 2001. There is no stain in the shirt, it\'s a cool JavaScript swag. I learned to accept what we have, a rich set of features, tolerable performance, good enough for many systems. Need more? Don\'t use ORM. Nothing is going to change dramatically, it\'s now as good as it can be\\n\\n### When will it shine?\\n\\n**Surely use Prisma under these scenarios -** If your data needs are rather simple; when time-to-market concern takes precedence over the data processing accuracy; when the DB is relatively small; if you\'re a mobile/frontend developer who is doing her first steps in the backend world; when there is a need for business-class support; AND when Prisma\'s long term business continuity risk is a non-issue for you\\n\\n**I\'d probably prefer other options under these conditions -** If the DB layer performance is a major concern; if you\'re savvy backend developer with solid SQL capabilities; when there is a need for fine grain control over the data layer. For all of these cases, Prisma might still work, but my primary choices would be using knex/TypeORM/Sequelize with a data-mapper style\\n\\nConsequently, we love Prisma and add it behind flag (--orm=prisma) to Practica.js. At the same time, until some clouds will disappear, Sequelize will remain our default ORM\\n\\n## Some of my other articles\\n\\n- [Book: Node.js testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [Book: JavaScript testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [Popular Node.js patterns and tools to re-consider](https://practica.dev/blog/popular-nodejs-pattern-and-tools-to-reconsider)\\n- [Practica.js - A Node.js starter](https://github.com/practicajs/practica)\\n- [Node.js best practices](https://github.com/goldbergyoni/nodebestpractices)"},{"id":"popular-nodejs-pattern-and-tools-to-reconsider","metadata":{"permalink":"/blog/popular-nodejs-pattern-and-tools-to-reconsider","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/pattern-to-reconsider/index.md","source":"@site/blog/pattern-to-reconsider/index.md","title":"Popular Node.js patterns and tools to re-consider","description":"Node.js is maturing. Many patterns and frameworks were embraced - it\'s my belief that developers\' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?","date":"2022-08-02T10:00:00.000Z","formattedDate":"August 2, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"nestjs","permalink":"/blog/tags/nestjs"},{"label":"fastify","permalink":"/blog/tags/fastify"},{"label":"passport","permalink":"/blog/tags/passport"},{"label":"dotenv","permalink":"/blog/tags/dotenv"},{"label":"supertest","permalink":"/blog/tags/supertest"},{"label":"practica","permalink":"/blog/tags/practica"},{"label":"testing","permalink":"/blog/tags/testing"}],"readingTime":21.09,"hasTruncateMarker":true,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"}],"frontMatter":{"slug":"popular-nodejs-pattern-and-tools-to-reconsider","date":"2022-08-02T10:00","hide_table_of_contents":true,"title":"Popular Node.js patterns and tools to re-consider","authors":["goldbergyoni"],"tags":["node.js","express","nestjs","fastify","passport","dotenv","supertest","practica","testing"]},"prevItem":{"title":"Is Prisma better than your \'traditional\' ORM?","permalink":"/blog/is-prisma-better-than-your-traditional-orm"},"nextItem":{"title":"Practica.js v0.0.1 is alive","permalink":"/blog/practica-is-alive"}},"content":"Node.js is maturing. Many patterns and frameworks were embraced - it\'s my belief that developers\' productivity dramatically increased in the past years. One downside of maturity is habits - we now reuse existing techniques more often. How is this a problem?\\n\\nIn his novel book \'Atomic Habits\' the author James Clear states that:\\n\\n> \\"Mastery is created by habits. However, sometimes when we\'re on auto-pilot performing habits, we tend to slip up... Just being we are gaining experience through performing the habits does not mean that we are improving. We actually go backwards on the improvement scale with most habits that turn into auto-pilot\\". In other words, practice makes perfect, and bad practices make things worst\\n\\nWe copy-paste mentally and physically things that we are used to, but these things are not necessarily right anymore. Like animals who shed their shells or skin to adapt to a new reality, so the Node.js community should constantly gauge its existing patterns, discuss and change\\n\\nLuckily, unlike other languages that are more committed to specific design paradigms (Java, Ruby) - Node is a house of many ideas. In this community, I feel safe to question some of our good-old tooling and patterns. The list below contains my personal beliefs, which are brought with reasoning and examples. \\n\\nAre those disruptive thoughts surely correct? I\'m not sure. There is one things I\'m sure about though - For Node.js to live longer, we need to encourage critics, focus our loyalty on innovation, and keep the discussion going. The outcome of this discussion is not \\"don\'t use this tool!\\" but rather becoming familiar with other techniques that, _under some circumstances_ might be a better fit\\n\\n![Animals and frameworks shed their skin](./crab.webp)\\n\\n_The True Crab\'s exoskeleton is hard and inflexible, he must shed his restrictive exoskeleton to grow and reveal the new roomier shell_\\n\\n\\n## TOC - Patterns to reconsider\\n\\n1. Dotenv\\n2. Calling a service from a controller\\n3. Nest.js dependency injection for all classes\\n4. Passport.js\\n5. Supertest\\n6. Fastify utility decoration\\n7. Logging from a catch clause\\n8. Morgan logger\\n9. NODE_ENV\\n\\n\x3c!--truncate--\x3e\\n## 1. Dotenv as your configuration source\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** A super popular technique in which the app configurable values (e.g., DB user name) are stored in a simple text file. Then, when the app loads, the dotenv library sets all the text file values as environment variables so the code can read this\\n\\n```javascript\\n// .env file\\nUSER_SERVICE_URL=https://users.myorg.com\\n\\n//start.js\\nrequire(\'dotenv\').config();\\n\\n//blog-post-service.js\\nrepository.savePost(post);\\n//update the user number of posts, read the users service URL from an environment variable\\nawait axios.put(`${process.env.USER_SERVICE_URL}/api/user/${post.userId}/incrementPosts`)\\n\\n```\\n\\n**\ud83d\udcca How popular:** 21,806,137 downloads/week!\\n\\n**\ud83e\udd14 Why it might be wrong:** Dotenv is so easy and intuitive to start with, so one might easily overlook fundamental features: For example, it\'s hard to infer the configuration schema and realize the meaning of each key and its typing. Consequently, there is no built-in way to fail fast when a mandatory key is missing - a flow might fail after starting and presenting some side effects (e.g., DB records were already mutated before the failure). In the example above, the blog post will be saved to DB, and only then will the code realize that a mandatory key is missing - This leaves the app hanging in an invalid state. On top of this, in the presence of many keys, it\'s impossible to organize them hierarchically. If not enough, it encourages developers to commit this .env file which might contain production values - this happens because there is no clear way to define development defaults. Teams usually work around this by committing .env.example file and then asking whoever pulls code to rename this file manually. If they remember to of course\\n\\n**\u2600\ufe0f Better alternative:** Some configuration libraries provide out of the box solution to all of these needs. They encourage a clear schema and the possibility to validate early and fail if needed. See [comparison of options here](https://practica.dev/decisions/configuration-library). One of the better alternatives is [\'convict\'](https://github.com/mozilla/node-convict), down below is the same example, this time with Convict, hopefully it\'s better now:\\n\\n```javascript\\n// config.js\\nexport default {\\n userService: {\\n url: {\\n // Hierarchical, documented and strongly typed \ud83d\udc47\\n doc: \\"The URL of the user management service including a trailing slash\\",\\n format: \\"url\\",\\n default: \\"http://localhost:4001\\",\\n nullable: false,\\n env: \\"USER_SERVICE_URL\\",\\n },\\n },\\n //more keys here\\n};\\n\\n//start.js\\nimport convict from \\"convict\\";\\nimport configSchema from \\"config\\";\\nconvict(configSchema);\\n// Fail fast!\\nconvictConfigurationProvider.validate();\\n\\n//blog-post.js\\nrepository.savePost(post);\\n// Will never arrive here if the URL is not set\\nawait axios.put(\\n `${convict.get(userService.url)}/api/user/${post.userId}/incrementPosts`\\n);\\n```\\n\\n## 2. Calling a \'fat\' service from the API controller\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Consider a reader of our code who wishes to understand the entire _high-level_ flow or delve into a very _specific_ part. She first lands on the API controller, where requests start. Unlike what its name implies, this controller layer is just an adapter and kept really thin and straightforward. Great thus far. Then the controller calls a big \'service\' with thousands of lines of code that represent the entire logic\\n\\n```javascript\\n// user-controller\\nrouter.post(\'/\', async (req, res, next) => {\\n await userService.add(req.body);\\n // Might have here try-catch or error response logic\\n}\\n\\n// user-service\\nexports function add(newUser){\\n // Want to understand quickly? Need to understand the entire user service, 1500 loc\\n // It uses technical language and reuse narratives of other flows\\n this.copyMoreFieldsToUser(newUser)\\n const doesExist = this.updateIfAlreadyExists(newUser)\\n if(!doesExist){\\n addToCache(newUser);\\n }\\n // 20 more lines that demand navigating to other functions in order to get the intent\\n}\\n\\n\\n```\\n\\n**\ud83d\udcca How popular:** It\'s hard to pull solid numbers here, I could confidently say that in _most_ of the app that I see, this is the case\\n\\n**\ud83e\udd14 Why it might be wrong:** We\'re here to tame complexities. One of the useful techniques is deferring a complexity to the later stage possible. In this case though, the reader of the code (hopefully) starts her journey through the tests and the controller - things are simple in these areas. Then, as she lands on the big service - she gets tons of complexity and small details, although she is focused on understanding the overall flow or some specific logic. This is **unnecessary** complexity\\n\\n**\u2600\ufe0f Better alternative:** The controller should call a particular type of service, a **use-case** , which is responsible for _summarizing_ the flow in a business and simple language. Each flow/feature is described using a use-case, each contains 4-10 lines of code, that tell the story without technical details. It mostly orchestrates other small services, clients, and repositories that hold all the implementation details. With use cases, the reader can grasp the high-level flow easily. She can now **choose** where she would like to focus. She is now exposed only to **necessary** complexity. This technique also encourages partitioning the code to the smaller object that the use-case orchestrates. Bonus: By looking at coverage reports, one can tell which features are covered, not just files/functions\\n\\nThis idea by the way is formalized in the [\'clean architecture\' book](https://www.bookdepository.com/Clean-Architecture-Robert-Martin/9780134494166?redirected=true&utm_medium=Google&utm_campaign=Base1&utm_source=IL&utm_content=Clean-Architecture&selectCurrency=ILS&w=AFF9AU99ZB4MTDA8VTRQ&gclid=Cj0KCQjw3eeXBhD7ARIsAHjssr92kqLn60dnfQCLjbkaqttdgvhRV5dqKtnY680GCNDvKp-16HtZp24aAg6GEALw_wcB) - I\'m not a big fan of \'fancy\' architectures, but see - it\'s worth cherry-picking techniques from every source. You may walk-through our [Node.js best practices starter, practica.js](https://github.com/practicajs/practica), and examine the use-cases code\\n\\n```javascript\\n// add-order-use-case.js\\nexport async function addOrder(newOrder: addOrderDTO) {\\n orderValidation.assertOrderIsValid(newOrder);\\n const userWhoOrdered = await userServiceClient.getUserWhoOrdered(\\n newOrder.userId\\n );\\n paymentTermsService.assertPaymentTerms(\\n newOrder.paymentTermsInDays,\\n userWhoOrdered.terms\\n );\\n\\n const response = await orderRepository.addOrder(newOrder);\\n\\n return response;\\n}\\n```\\n\\n## 3. Nest.js: Wire _everything_ with dependency injection\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** If you\'re doing Nest.js, besides having a powerful framework in your hands, you probably use DI for _everything_ and make every class injectable. Say you have a weather-service that depends upon humidity-service, and **there is no requirement to swap** the humidity-service with alternative providers. Nevertheless, you inject humidity-service into the weather-service. It becomes part of your development style, \\"why not\\" you think - I may need to stub it during testing or replace it in the future\\n\\n```typescript\\n// humidity-service.ts - not customer facing\\n@Injectable()\\nexport class GoogleHumidityService {\\n\\n async getHumidity(when: Datetime): Promise {\\n // Fetches from some specific cloud service\\n }\\n}\\n\\n// weather-service.ts - customer facing\\nimport { GoogleHumidityService } from \'./humidity-service.ts\';\\n\\nexport type weatherInfo{\\n temperature: number,\\n humidity: number\\n}\\n\\nexport class WeatherService {\\n constructor(private humidityService: GoogleHumidityService) {}\\n\\n async GetWeather(when: Datetime): Promise {\\n // Fetch temperature from somewhere and then humidity from GoogleHumidityService\\n }\\n}\\n\\n// app.module.ts\\n@Module({\\n providers: [GoogleHumidityService, WeatherService],\\n})\\nexport class AppModule {}\\n```\\n\\n**\ud83d\udcca How popular:** No numbers here but I could confidently say that in _all_ of the Nest.js app that I\'ve seen, this is the case. In the popular [\'nestjs-realworld-example-ap[p\'](](https://github.com/lujakob/nestjs-realworld-example-app)) all the services are \'injectable\'\\n\\n**\ud83e\udd14 Why it might be wrong:** Dependency injection is not a priceless coding style but a pattern you should pull in the right moment, like any other pattern. Why? Because any pattern has a price. What price, you ask? First, encapsulation is violated. Clients of the weather-service are now aware that other providers are being used _internally_. Some clients may get tempted to override providers also it\'s not under their responsibility. Second, it\'s another layer of complexity to learn, maintain, and one more way to shoot yourself in the legs. StackOverflow owes some of its revenues to Nest.js DI - plenty of discussions try to solve this puzzle (e.g. did you know that in case of circular dependencies the order of imports matters?). Third, there is the performance thing - Nest.js, for example struggled to provide a decent start time for serverless environments and had to introduce [lazy loaded modules](https://docs.nestjs.com/fundamentals/lazy-loading-modules). Don\'t get me wrong, **in some cases**, there is a good case for DI: When a need arises to decouple a dependency from its caller, or to allow clients to inject custom implementations (e.g., the strategy pattern). **In such case**, when there is a value, you may consider whether the _value of DI is worth its price_. If you don\'t have this case, why pay for nothing?\\n\\nI recommend reading the first paragraphs of this blog post [\'Dependency Injection is EVIL\'](https://www.tonymarston.net/php-mysql/dependency-injection-is-evil.html) (and absolutely don\'t agree with this bold words)\\n\\n**\u2600\ufe0f Better alternative:** \'Lean-ify\' your engineering approach - avoid using any tool unless it serves a real-world need immediately. Start simple, a dependent class should simply import its dependency and use it - Yeah, using the plain Node.js module system (\'require\'). Facing a situation when there is a need to factor dynamic objects? There are a handful of simple patterns, simpler than DI, that you should consider, like \'if/else\', factory function, and more. Are singletons requested? Consider techniques with lower costs like the module system with factory function. Need to stub/mock for testing? Monkey patching might be better than DI: better clutter your test code a bit than clutter your production code. Have a strong need to hide from an object where its dependencies are coming from? You sure? Use DI!\\n\\n```typescript\\n// humidity-service.ts - not customer facing\\nexport async function getHumidity(when: Datetime): Promise {\\n // Fetches from some specific cloud service\\n}\\n\\n// weather-service.ts - customer facing\\nimport { getHumidity } from \\"./humidity-service.ts\\";\\n\\n// \u2705 No wiring is happening externally, all is flat and explicit. Simple\\nexport async function getWeather(when: Datetime): Promise {\\n // Fetch temperature from somewhere and then humidity from GoogleHumidityService\\n // Nobody needs to know about it, its an implementation details\\n await getHumidity(when);\\n}\\n```\\n\\n___\\n\\n## 1 min pause: A word or two about me, the author\\n\\nMy name is Yoni Goldberg, I\'m a Node.js developer and consultant. I wrote few code-books like [JavaScript testing best practices](https://github.com/goldbergyoni/javascript-testing-best-practices) and [Node.js best practices](https://github.com/goldbergyoni/nodebestpractices) (100,000 stars \u2728\ud83e\udd79). That said, my best guide is [Node.js testing practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices) which only few read \ud83d\ude1e. I shall release [an advanced Node.js testing course soon](https://testjavascript.com/) and also hold workshops for teams. I\'m also a core maintainer of [Practica.js](https://github.com/practicajs/practica) which is a Node.js starter that creates a production-ready example Node Monorepo solution that is based on the standards and simplicity. It might be your primary option when starting a new Node.js solution\\n\\n___\\n\\n## 4. Passport.js for token authentication\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** Commonly, you\'re in need to issue or/and authenticate JWT tokens. Similarly, you might need to allow login from _one_ single social network like Google/Facebook. When faced with these kinds of needs, Node.js developers rush to the glorious library [Passport.js](https://www.passportjs.org/) like butterflies are attracted to light\\n\\n**\ud83d\udcca How popular:** 1,389,720 weekly downloads\\n\\n**\ud83e\udd14 Why it might be wrong:** When tasked with guarding your routes with JWT token - you\'re just a few lines of code shy from ticking the goal. Instead of messing up with a new framework, instead of introducing levels of indirections (you call passport, then it calls you), instead of spending time learning new abstractions - use a JWT library directly. Libraries like [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) or [fast-jwt](https://github.com/nearform/fast-jwt) are simple and well maintained. Have concerns with the security hardening? Good point, your concerns are valid. But would you not get better hardening with a direct understanding of your configuration and flow? Will hiding things behind a framework help? Even if you prefer the hardening of a battle-tested framework, Passport doesn\'t handle a handful of security risks like secrets/token, secured user management, DB protection, and more. My point, you probably anyway need fully-featured user and authentication management platforms. Various cloud services and OSS projects, can tick all of those security concerns. Why then start in the first place with a framework that doesn\'t satisfy your security needs? It seems like many who opt for Passport.js are not fully aware of which needs are satisfied and which are left open. All of that said, Passport definitely shines when looking for a quick way to support _many_ social login providers\\n\\n**\u2600\ufe0f Better alternative:** Is token authentication in order? These few lines of code below might be all you need. You may also glimpse into [Practica.js wrapper around these libraries](https://github.com/practicajs/practica/tree/main/src/code-templates/libraries/jwt-token-verifier). A real-world project at scale typically need more: supporting async JWT [(JWKS)](https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets), securely manage and rotate the secrets to name a few examples. In this case, OSS solution like [keycloak (https://github.com/keycloak/keycloak) or commercial options like Auth0[https://github.com/auth0] are alternatives to consider\\n\\n```javascript\\n// jwt-middleware.js, a simplified version - Refer to Practica.js to see some more corner cases\\nconst middleware = (req, res, next) => {\\n if(!req.headers.authorization){\\n res.sendStatus(401)\\n }\\n\\n jwt.verify(req.headers.authorization, options.secret, (err: any, jwtContent: any) => {\\n if (err) {\\n return res.sendStatus(401);\\n }\\n\\n req.user = jwtContent.data;\\n\\n next();\\n });\\n```\\n\\n## 5. Supertest for integration/API testing\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** When testing against an API (i.e., component, integration, E2E tests), the library [supertest](https://www.npmjs.com/package/supertest) provides a sweet syntax that can both detect the web server address, make HTTP call and also assert on the response. Three in one\\n\\n```javascript\\ntest(\\"When adding invalid user, then the response is 400\\", (done) => {\\n const request = require(\\"supertest\\");\\n const app = express();\\n // Arrange\\n const userToAdd = {\\n name: undefined,\\n };\\n\\n // Act\\n request(app)\\n .post(\\"/user\\")\\n .send(userToAdd)\\n .expect(\\"Content-Type\\", /json/)\\n .expect(400, done);\\n\\n // Assert\\n // We already asserted above \u261d\ud83c\udffb as part of the request\\n});\\n```\\n\\n**\ud83d\udcca How popular:** 2,717,744 weekly downloads\\n\\n**\ud83e\udd14 Why it might be wrong:** You already have your assertion library (Jest? Chai?), it has a great error highlighting and comparison - you trust it. Why code some tests using another assertion syntax? Not to mention, Supertest\'s assertion errors are not as descriptive as Jest and Chai. It\'s also cumbersome to mix HTTP client + assertion library instead of choosing the best for each mission. Speaking of the best, there are more standard, popular, and better-maintained HTTP clients (like fetch, axios and other friends). Need another reason? Supertest might encourage coupling the tests to Express as it offers a constructor that gets an Express object. This constructor infers the API address automatically (useful when using dynamic test ports). This couples the test to the implementation and won\'t work in the case where you wish to run the same tests against a remote process (the API doesn\'t live with the tests). My repository [\'Node.js testing best practices\'](https://github.com/testjavascript/nodejs-integration-tests-best-practices) holds examples of how tests can infer the API port and address\\n\\n**\u2600\ufe0f Better alternative:** A popular and standard HTTP client library like Node.js Fetch or Axios. In [Practica.js](https://github.com/practicajs/practica) (a Node.js starter that packs many best practices) we use Axios. It allows us to configure a HTTP client that is shared among all the tests: We bake inside a JWT token, headers, and a base URL. Another good pattern that we look at, is making each Microservice generate HTTP client library for its consumers. This brings strong-type experience to the clients, synchronizes the provider-consumer versions and as a bonus - The provider can test itself with the same library that its consumers are using\\n\\n```javascript\\ntest(\\"When adding invalid user, then the response is 400 and includes a reason\\", (done) => {\\n const app = express();\\n // Arrange\\n const userToAdd = {\\n name: undefined,\\n };\\n\\n // Act\\n const receivedResponse = axios.post(\\n `http://localhost:${apiPort}/user`,\\n userToAdd\\n );\\n\\n // Assert\\n // \u2705 Assertion happens in a dedicated stage and a dedicated library\\n expect(receivedResponse).toMatchObject({\\n status: 400,\\n data: {\\n reason: \\"no-name\\",\\n },\\n });\\n});\\n```\\n\\n## 6. Fastify decorate for non request/web utilities\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** [Fastify](https://github.com/fastify/fastify) introduces great patterns. Personally, I highly appreciate how it preserves the simplicity of Express while bringing more batteries. One thing that got me wondering is the \'decorate\' feature which allows placing common utilities/services inside a widely accessible container object. I\'m referring here specifically to the case where a cross-cutting concern utility/service is being used. Here is an example:\\n\\n```javascript\\n// An example of a utility that is cross-cutting-concern. Could be logger or anything else\\nfastify.decorate(\'metricsService\', function (name) {\\n fireMetric: () => {\\n // My code that sends metrics to the monitoring system\\n }\\n})\\n\\nfastify.get(\'/api/orders\', async function (request, reply) {\\n this.metricsService.fireMetric({name: \'new-request\'})\\n // Handle the request\\n})\\n\\n// my-business-logic.js\\nexports function calculateSomething(){\\n // How to fire a metric?\\n}\\n```\\n\\nIt should be noted that \'decoration\' is also used to place values (e.g., user) inside a request - this is a slightly different case and a sensible one\\n\\n**\ud83d\udcca How popular:** Fastify has 696,122 weekly download and growing rapidly. The decorator concept is part of the framework\'s core\\n\\n**\ud83e\udd14 Why it might be wrong:** Some services and utilities serve cross-cutting-concern needs and should be accessible from other layers like domain (i.e, business logic, DAL). When placing utilities inside this object, the Fastify object might not be accessible to these layers. You probably don\'t want to couple your web framework with your business logic: Consider that some of your business logic and repositories might get invoked from non-REST clients like CRON, MQ, and similar - In these cases, Fastify won\'t get involved at all so better not trust it to be your service locator\\n\\n**\u2600\ufe0f Better alternative:** A good old Node.js module is a standard way to expose and consume functionality. Need a singleton? Use the module system caching. Need to instantiate a service in correlation with a Fastify life-cycle hook (e.g., DB connection on start)? Call it from that Fastify hook. In the rare case where a highly dynamic and complex instantiation of dependencies is needed - DI is also a (complex) option to consider\\n\\n```javascript\\n// \u2705 A simple usage of good old Node.js modules\\n// metrics-service.js\\n\\nexports async function fireMetric(name){\\n // My code that sends metrics to the monitoring system\\n}\\n\\nimport {fireMetric} from \'./metrics-service.js\'\\n\\nfastify.get(\'/api/orders\', async function (request, reply) {\\n metricsService.fireMetric({name: \'new-request\'})\\n})\\n\\n// my-business-logic.js\\nexports function calculateSomething(){\\n metricsService.fireMetric({name: \'new-request\'})\\n}\\n```\\n\\n## 7. Logging from a catch clause\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** You catch an error somewhere deep in the code (not on the route level), then call logger.error to make this error observable. Seems simple and necessary\\n\\n```javascript\\ntry{\\n axios.post(\'https://thatService.io/api/users);\\n}\\ncatch(error){\\n logger.error(error, this, {operation: addNewOrder});\\n}\\n```\\n\\n**\ud83d\udcca How popular:** Hard to put my hands on numbers but it\'s quite popular, right?\\n\\n**\ud83e\udd14 Why it might be wrong:** First, errors should get handled/logged in a central location. Error handling is a critical path. Various catch clauses are likely to behave differently without a centralized and unified behavior. For example, a request might arise to tag all errors with certain metadata, or on top of logging, to also fire a monitoring metric. Applying these requirements in ~100 locations is not a walk in the park. Second, catch clauses should be minimized to particular scenarios. By default, the natural flow of an error is bubbling down to the route/entry-point - from there, it will get forwarded to the error handler. Catch clauses are more verbose and error-prone - therefore it should serve two very specific needs: When one wishes to change the flow based on the error or enrich the error with more information (which is not the case in this example)\\n\\n**\u2600\ufe0f Better alternative:** By default, let the error bubble down the layers and get caught by the entry-point global catch (e.g., Express error middleware). In cases when the error should trigger a different flow (e.g., retry) or there is value in enriching the error with more context - use a catch clause. In this case, ensure the .catch code also reports to the error handler\\n\\n```javascript\\n// A case where we wish to retry upon failure\\ntry{\\n axios.post(\'https://thatService.io/api/users);\\n}\\ncatch(error){\\n // \u2705 A central location that handles error\\n errorHandler.handle(error, this, {operation: addNewOrder});\\n callTheUserService(numOfRetries++);\\n}\\n```\\n\\n## 8. Use Morgan logger for express web requests\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** In many web apps, you are likely to find a pattern that is being copy-pasted for ages - Using Morgan logger to log requests information:\\n\\n```javascript\\nconst express = require(\\"express\\");\\nconst morgan = require(\\"morgan\\");\\n\\nconst app = express();\\n\\napp.use(morgan(\\"combined\\"));\\n```\\n\\n**\ud83d\udcca How popular:** 2,901,574 downloads/week\\n\\n**\ud83e\udd14 Why it might be wrong:** Wait a second, you already have your main logger, right? Is it Pino? Winston? Something else? Great. Why deal with and configure yet another logger? I do appreciate the HTTP domain-specific language (DSL) of Morgan. The syntax is sweet! But does it justify having two loggers?\\n\\n**\u2600\ufe0f Better alternative:** Put your chosen logger in a middleware and log the desired request/response properties:\\n\\n```javascript\\n// \u2705 Use your preferred logger for all the tasks\\nconst logger = require(\\"pino\\")();\\napp.use((req, res, next) => {\\n res.on(\\"finish\\", () => {\\n logger.info(`${req.url} ${res.statusCode}`); // Add other properties here\\n });\\n next();\\n});\\n```\\n\\n## 9. Having conditional code based on `NODE_ENV` value\\n\\n**\ud83d\udc81\u200d\u2642\ufe0f What is it about:** To differentiate between development vs production configuration, it\'s common to set the environment variable NODE_ENV with \\"production|test\\". Doing so allows the various tooling to act differently. For example, some templating engines will cache compiled templates only in production. Beyond tooling, custom applications use this to specify behaviours that are unique to the development or production environment:\\n\\n```javascript\\nif (process.env.NODE_ENV === \\"production\\") {\\n // This is unlikely to be tested since test runner usually set NODE_ENV=test\\n setLogger({ stdout: true, prettyPrint: false });\\n // If this code branch above exists, why not add more production-only configurations:\\n collectMetrics();\\n} else {\\n setLogger({ splunk: true, prettyPrint: true });\\n}\\n```\\n\\n**\ud83d\udcca How popular:** 5,034,323 code results in GitHub when searching for \\"NODE_ENV\\". It doesn\'t seem like a rare pattern\\n\\n**\ud83e\udd14 Why it might be wrong:** Anytime your code checks whether it\'s production or not, this branch won\'t get hit by default in some test runner (e.g., Jest set `NODE_ENV=test`). In _any_ test runner, the developer must remember to test for each possible value of this environment variable. In the example above, `collectMetrics()` will be tested for the first time in production. Sad smiley. Additionally, putting these conditions opens the door to add more differences between production and the developer machine - when this variable and conditions exists, a developer gets tempted to put some logic for production only. Theoretically, this can be tested: one can set `NODE_ENV = \\"production\\"` in testing and cover the production branches (if she remembers...). But then, if you can test with `NODE_ENV=\'production\'`, what\'s the point in separating? Just consider everything to be \'production\' and avoid this error-prone mental load\\n\\n**\u2600\ufe0f Better alternative:** Any code that was written by us, must be tested. This implies avoiding any form of if(production)/else(development) conditions. Wouldn\'t anyway developers machine have different surrounding infrastructure than production (e.g., logging system)? They do, the environments are quite difference, but we feel comfortable with it. These infrastructural things are battle-tested, extraneous, and not part of our code. To keep the same code between dev/prod and still use different infrastructure - we put different values in the configuration (not in the code). For example, a typical logger emits JSON in production but in a development machine it emits \'pretty-print\' colorful lines. To meet this, we set ENV VAR that tells whether what logging style we aim for:\\n\\n```javascript\\n//package.json\\n\\"scripts\\": {\\n \\"start\\": \\"LOG_PRETTY_PRINT=false index.js\\",\\n \\"test\\": \\"LOG_PRETTY_PRINT=true jest\\"\\n}\\n\\n//index.js\\n//\u2705 No condition, same code for all the environments. The variations are defined externally in config or deployment files\\nsetLogger({prettyPrint: process.env.LOG_PRETTY_PRINT})\\n```\\n\\n## Closing\\n\\nI hope that these thoughts, at least one of them, made you re-consider adding a new technique to your toolbox. In any case, let\'s keep our community vibrant, disruptive and kind. Respectful discussions are almost as important as the event loop. Almost.\\n\\n## Some of my other articles\\n\\n- [Book: Node.js testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [Book: JavaScript testing best practices](https://github.com/testjavascript/nodejs-integration-tests-best-practices)\\n- [How to be a better Node.js developer in 2020](https://yonigoldberg.medium.com/20-ways-to-become-a-better-node-js-developer-in-2020-d6bd73fcf424). The 2023 version is coming soon\\n- [Practica.js - A Node.js starter](https://github.com/practicajs/practica)\\n- [Node.js best practices](https://github.com/goldbergyoni/nodebestpractices)"},{"id":"practica-is-alive","metadata":{"permalink":"/blog/practica-is-alive","editUrl":"https://github.com/practicajs/practica/tree/main/docs/blog/practica-is-alive/index.md","source":"@site/blog/practica-is-alive/index.md","title":"Practica.js v0.0.1 is alive","description":"\ud83e\udd73 We\'re thrilled to launch the very first version of Practica.js.","date":"2022-07-15T10:00:00.000Z","formattedDate":"July 15, 2022","tags":[{"label":"node.js","permalink":"/blog/tags/node-js"},{"label":"express","permalink":"/blog/tags/express"},{"label":"fastify","permalink":"/blog/tags/fastify"}],"readingTime":1.21,"hasTruncateMarker":false,"authors":[{"name":"Yoni Goldberg","title":"Practica.js core maintainer","url":"https://github.com/goldbergyoni","imageURL":"https://github.com/goldbergyoni.png","key":"goldbergyoni"}],"frontMatter":{"slug":"practica-is-alive","date":"2022-07-15T10:00","hide_table_of_contents":true,"title":"Practica.js v0.0.1 is alive","authors":["goldbergyoni"],"tags":["node.js","express","fastify"]},"prevItem":{"title":"Popular Node.js patterns and tools to re-consider","permalink":"/blog/popular-nodejs-pattern-and-tools-to-reconsider"}},"content":"\ud83e\udd73 We\'re thrilled to launch the very first version of Practica.js.\\n\\n## What is Practica is one paragraph\\n\\nAlthough Node.js has great frameworks \ud83d\udc9a, they were never meant to be production ready immediately. Practica.js aims to bridge the gap. Based on your preferred framework, we generate some example code that demonstrates a full workflow, from API to DB, that is packed with good practices. For example, we include a hardened dockerfile, N-Tier folder structure, great testing templates, and more. This saves a great deal of time and can prevent painful mistakes. All decisions made are [neatly and thoughtfully documented](./decisions/index). We strive to keep things as simple and standard as possible and base our work off the popular guide: [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices).\\n\\nYour developer experience would look as follows: Generate our starter using the CLI and get an example Node.js solution. This solution is a typical Monorepo setup with an example Microservice and libraries. All is based on super-popular libraries that we merely stitch together. It also constitutes tons of optimization - linters, libraries, Monorepo configuration, tests and much more. Inside the example Microservice you\'ll find an example flow, from API to DB. Based on this, you can modify the entity and DB fields and build you app. \\n\\n## 90 seconds video\\n\\n\\n\\n## How to get started\\n\\nTo get up to speed quickly, read our [getting started guide](https://practica.dev/the-basics/getting-started-quickly)."}]}')}}]); \ No newline at end of file diff --git a/assets/js/d2a399e8.bd08fe86.js b/assets/js/d2a399e8.bd08fe86.js deleted file mode 100644 index f4735d97..00000000 --- a/assets/js/d2a399e8.bd08fe86.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[366],{3905:(e,o,t)=>{t.d(o,{Zo:()=>p,kt:()=>m});var r=t(7294);function n(e,o,t){return o in e?Object.defineProperty(e,o,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[o]=t,e}function a(e,o){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);o&&(r=r.filter((function(o){return Object.getOwnPropertyDescriptor(e,o).enumerable}))),t.push.apply(t,r)}return t}function i(e){for(var o=1;o=0||(n[t]=e[t]);return n}(e,o);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(n[t]=e[t])}return n}var l=r.createContext({}),c=function(e){var o=r.useContext(l),t=o;return e&&(t="function"==typeof e?e(o):i(i({},o),e)),t},p=function(e){var o=c(e.components);return r.createElement(l.Provider,{value:o},e.children)},h={inlineCode:"code",wrapper:function(e){var o=e.children;return r.createElement(r.Fragment,{},o)}},u=r.forwardRef((function(e,o){var t=e.components,n=e.mdxType,a=e.originalType,l=e.parentName,p=s(e,["components","mdxType","originalType","parentName"]),u=c(t),m=n,d=u["".concat(l,".").concat(m)]||u[m]||h[m]||a;return t?r.createElement(d,i(i({ref:o},p),{},{components:t})):r.createElement(d,i({ref:o},p))}));function m(e,o){var t=arguments,n=o&&o.mdxType;if("string"==typeof e||n){var a=t.length,i=new Array(a);i[0]=u;var s={};for(var l in o)hasOwnProperty.call(o,l)&&(s[l]=o[l]);s.originalType=e,s.mdxType="string"==typeof e?e:n,i[1]=s;for(var c=2;c{t.r(o),t.d(o,{assets:()=>l,contentTitle:()=>i,default:()=>h,frontMatter:()=>a,metadata:()=>s,toc:()=>c});var r=t(7462),n=(t(7294),t(3905));const a={slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},i="Which Monorepo is right for a Node.js BACKEND\xa0now?",s={permalink:"/blog/monorepo-backend",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/which-monorepo/index.md",source:"@site/blog/which-monorepo/index.md",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",description:"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling",date:"2023-07-07T05:38:19.000Z",formattedDate:"July 7, 2023",tags:[{label:"monorepo",permalink:"/blog/tags/monorepo"},{label:"decisions",permalink:"/blog/tags/decisions"}],readingTime:16.925,hasTruncateMarker:!0,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Michael Salomon",title:"Practica.js core maintainer",url:"https://github.com/mikicho",imageURL:"https://avatars.githubusercontent.com/u/11459632?v=4",key:"michaelsalomon"}],frontMatter:{slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},prevItem:{title:"Testing the dark scenarios of your Node.js application",permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application"},nextItem:{title:"Practica v0.0.6 is alive",permalink:"/blog/practica-v0.0.6-is-alive"}},l={authorsImageUrls:[void 0,void 0]},c=[{value:"What are we looking\xa0at",id:"what-are-we-lookingat",level:2}],p={toc:c};function h(e){let{components:o,...a}=e;return(0,n.kt)("wrapper",(0,r.Z)({},p,a,{components:o,mdxType:"MDXLayout"}),(0,n.kt)("p",null,"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/practicajs/practica"},"Practica.js"),". In this post, we'd like to share our considerations in choosing our monorepo tooling"),(0,n.kt)("p",null,(0,n.kt)("img",{alt:"Monorepos",src:t(1377).Z,width:"1400",height:"796"})),(0,n.kt)("h2",{id:"what-are-we-lookingat"},"What are we looking\xa0at"),(0,n.kt)("p",null,"The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries \u2014 ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/lerna/lerna/issues/2703"},"Lerna- has just retired.")," When looking closely, it might not be just a coincidence \u2014 With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused \u2014 What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you\u2019re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow."),(0,n.kt)("p",null,"This post is concerned with backend-only and Node.js. It also scoped to ",(0,n.kt)("em",{parentName:"p"},"typical")," business solutions. If you\u2019re Google/FB developer who is faced with 8,000 packages \u2014 sorry, you need special gear. Consequently, monster Monorepo tooling like ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/thundergolfer/example-bazel-monorepo"},"Bazel")," is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it\u2019s not actually maintained anymore \u2014 it\u2019s a good baseline for comparison)."),(0,n.kt)("p",null,"Let\u2019s start? When human beings use the term Monorepo, they typically refer to one or more of the following ",(0,n.kt)("em",{parentName:"p"},"4 layers below.")," Each one of them can bring value to your project, each has different consequences, tooling, and features:"))}h.isMDXComponent=!0},1377:(e,o,t)=>{t.d(o,{Z:()=>r});const r=t.p+"assets/images/monorepo-high-level-291b29cc962144a43d78143889ba5d3b.png"}}]); \ No newline at end of file diff --git a/assets/js/d2a399e8.cbf45e11.js b/assets/js/d2a399e8.cbf45e11.js new file mode 100644 index 00000000..a355b02b --- /dev/null +++ b/assets/js/d2a399e8.cbf45e11.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[366],{3905:(e,o,t)=>{t.d(o,{Zo:()=>p,kt:()=>m});var r=t(7294);function n(e,o,t){return o in e?Object.defineProperty(e,o,{value:t,enumerable:!0,configurable:!0,writable:!0}):e[o]=t,e}function a(e,o){var t=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);o&&(r=r.filter((function(o){return Object.getOwnPropertyDescriptor(e,o).enumerable}))),t.push.apply(t,r)}return t}function i(e){for(var o=1;o=0||(n[t]=e[t]);return n}(e,o);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,t)&&(n[t]=e[t])}return n}var l=r.createContext({}),c=function(e){var o=r.useContext(l),t=o;return e&&(t="function"==typeof e?e(o):i(i({},o),e)),t},p=function(e){var o=c(e.components);return r.createElement(l.Provider,{value:o},e.children)},h={inlineCode:"code",wrapper:function(e){var o=e.children;return r.createElement(r.Fragment,{},o)}},u=r.forwardRef((function(e,o){var t=e.components,n=e.mdxType,a=e.originalType,l=e.parentName,p=s(e,["components","mdxType","originalType","parentName"]),u=c(t),m=n,d=u["".concat(l,".").concat(m)]||u[m]||h[m]||a;return t?r.createElement(d,i(i({ref:o},p),{},{components:t})):r.createElement(d,i({ref:o},p))}));function m(e,o){var t=arguments,n=o&&o.mdxType;if("string"==typeof e||n){var a=t.length,i=new Array(a);i[0]=u;var s={};for(var l in o)hasOwnProperty.call(o,l)&&(s[l]=o[l]);s.originalType=e,s.mdxType="string"==typeof e?e:n,i[1]=s;for(var c=2;c{t.r(o),t.d(o,{assets:()=>l,contentTitle:()=>i,default:()=>h,frontMatter:()=>a,metadata:()=>s,toc:()=>c});var r=t(7462),n=(t(7294),t(3905));const a={slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},i="Which Monorepo is right for a Node.js BACKEND\xa0now?",s={permalink:"/blog/monorepo-backend",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/which-monorepo/index.md",source:"@site/blog/which-monorepo/index.md",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",description:"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling",date:"2023-07-11T13:19:18.000Z",formattedDate:"July 11, 2023",tags:[{label:"monorepo",permalink:"/blog/tags/monorepo"},{label:"decisions",permalink:"/blog/tags/decisions"}],readingTime:16.925,hasTruncateMarker:!0,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Michael Salomon",title:"Practica.js core maintainer",url:"https://github.com/mikicho",imageURL:"https://avatars.githubusercontent.com/u/11459632?v=4",key:"michaelsalomon"}],frontMatter:{slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},nextItem:{title:"Testing the dark scenarios of your Node.js application",permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application"}},l={authorsImageUrls:[void 0,void 0]},c=[{value:"What are we looking\xa0at",id:"what-are-we-lookingat",level:2}],p={toc:c};function h(e){let{components:o,...a}=e;return(0,n.kt)("wrapper",(0,r.Z)({},p,a,{components:o,mdxType:"MDXLayout"}),(0,n.kt)("p",null,"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/practicajs/practica"},"Practica.js"),". In this post, we'd like to share our considerations in choosing our monorepo tooling"),(0,n.kt)("p",null,(0,n.kt)("img",{alt:"Monorepos",src:t(1377).Z,width:"1400",height:"796"})),(0,n.kt)("h2",{id:"what-are-we-lookingat"},"What are we looking\xa0at"),(0,n.kt)("p",null,"The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries \u2014 ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/lerna/lerna/issues/2703"},"Lerna- has just retired.")," When looking closely, it might not be just a coincidence \u2014 With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused \u2014 What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you\u2019re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow."),(0,n.kt)("p",null,"This post is concerned with backend-only and Node.js. It also scoped to ",(0,n.kt)("em",{parentName:"p"},"typical")," business solutions. If you\u2019re Google/FB developer who is faced with 8,000 packages \u2014 sorry, you need special gear. Consequently, monster Monorepo tooling like ",(0,n.kt)("a",{parentName:"p",href:"https://github.com/thundergolfer/example-bazel-monorepo"},"Bazel")," is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it\u2019s not actually maintained anymore \u2014 it\u2019s a good baseline for comparison)."),(0,n.kt)("p",null,"Let\u2019s start? When human beings use the term Monorepo, they typically refer to one or more of the following ",(0,n.kt)("em",{parentName:"p"},"4 layers below.")," Each one of them can bring value to your project, each has different consequences, tooling, and features:"))}h.isMDXComponent=!0},1377:(e,o,t)=>{t.d(o,{Z:()=>r});const r=t.p+"assets/images/monorepo-high-level-291b29cc962144a43d78143889ba5d3b.png"}}]); \ No newline at end of file diff --git a/assets/js/dac877fa.9df0ffd6.js b/assets/js/dac877fa.c7e0b7f7.js similarity index 52% rename from assets/js/dac877fa.9df0ffd6.js rename to assets/js/dac877fa.c7e0b7f7.js index a02a1e2d..b478edc2 100644 --- a/assets/js/dac877fa.9df0ffd6.js +++ b/assets/js/dac877fa.c7e0b7f7.js @@ -1 +1 @@ -"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[1867],{3905:(e,t,a)=>{a.d(t,{Zo:()=>p,kt:()=>m});var r=a(7294);function i(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function n(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,r)}return a}function o(e){for(var t=1;t=0||(i[a]=e[a]);return i}(e,t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(i[a]=e[a])}return i}var s=r.createContext({}),c=function(e){var t=r.useContext(s),a=t;return e&&(a="function"==typeof e?e(t):o(o({},t),e)),a},p=function(e){var t=c(e.components);return r.createElement(s.Provider,{value:t},e.children)},u={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},d=r.forwardRef((function(e,t){var a=e.components,i=e.mdxType,n=e.originalType,s=e.parentName,p=l(e,["components","mdxType","originalType","parentName"]),d=c(a),m=i,h=d["".concat(s,".").concat(m)]||d[m]||u[m]||n;return a?r.createElement(h,o(o({ref:t},p),{},{components:a})):r.createElement(h,o({ref:t},p))}));function m(e,t){var a=arguments,i=t&&t.mdxType;if("string"==typeof e||i){var n=a.length,o=new Array(n);o[0]=d;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l.mdxType="string"==typeof e?e:i,o[1]=l;for(var c=2;c{a.r(t),a.d(t,{assets:()=>s,contentTitle:()=>o,default:()=>u,frontMatter:()=>n,metadata:()=>l,toc:()=>c});var r=a(7462),i=(a(7294),a(3905));const n={slug:"practica-v0.0.6-is-alive",date:"2022-12-10T10:00",hide_table_of_contents:!0,title:"Practica v0.0.6 is alive",authors:["goldbergyoni","razluvaton","danielgluskin","michaelsalomon"],tags:["node.js","express","practica","prisma"]},o=void 0,l={permalink:"/blog/practica-v0.0.6-is-alive",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/v0.6-is-alive/index.md",source:"@site/blog/v0.6-is-alive/index.md",title:"Practica v0.0.6 is alive",description:"Where is our focus now?",date:"2022-12-10T10:00:00.000Z",formattedDate:"December 10, 2022",tags:[{label:"node.js",permalink:"/blog/tags/node-js"},{label:"express",permalink:"/blog/tags/express"},{label:"practica",permalink:"/blog/tags/practica"},{label:"prisma",permalink:"/blog/tags/prisma"}],readingTime:1.47,hasTruncateMarker:!1,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Raz Luvaton",title:"Practica.js core maintainer",url:"https://github.com/rluvaton",imageURL:"https://avatars.githubusercontent.com/u/16746759?v=4",key:"razluvaton"},{name:"Daniel Gluskin",title:"Practica.js core maintainer",url:"https://github.com/DanielGluskin",imageURL:"https://avatars.githubusercontent.com/u/17989958?v=4",key:"danielgluskin"},{name:"Michael Salomon",title:"Practica.js core maintainer",url:"https://github.com/mikicho",imageURL:"https://avatars.githubusercontent.com/u/11459632?v=4",key:"michaelsalomon"}],frontMatter:{slug:"practica-v0.0.6-is-alive",date:"2022-12-10T10:00",hide_table_of_contents:!0,title:"Practica v0.0.6 is alive",authors:["goldbergyoni","razluvaton","danielgluskin","michaelsalomon"],tags:["node.js","express","practica","prisma"]},prevItem:{title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",permalink:"/blog/monorepo-backend"},nextItem:{title:"Is Prisma better than your 'traditional' ORM?",permalink:"/blog/is-prisma-better-than-your-traditional-orm"}},s={authorsImageUrls:[void 0,void 0,void 0,void 0]},c=[{value:"Where is our focus now?",id:"where-is-our-focus-now",level:2},{value:"What's new?",id:"whats-new",level:2},{value:"Request-level store",id:"request-level-store",level:3},{value:"Hardened .dockerfile",id:"hardened-dockerfile",level:3},{value:"Additional ORM option: Prisma",id:"additional-orm-option-prisma",level:3},{value:"Many small enhancements",id:"many-small-enhancements",level:3},{value:"Where do I start?",id:"where-do-i-start",level:2}],p={toc:c};function u(e){let{components:t,...a}=e;return(0,i.kt)("wrapper",(0,r.Z)({},p,a,{components:t,mdxType:"MDXLayout"}),(0,i.kt)("h2",{id:"where-is-our-focus-now"},"Where is our focus now?"),(0,i.kt)("p",null,"We work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback"),(0,i.kt)("h2",{id:"whats-new"},"What's new?"),(0,i.kt)("h3",{id:"request-level-store"},"Request-level store"),(0,i.kt)("p",null,"Every request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is 'request-id' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in ",(0,i.kt)("a",{parentName:"p",href:"https://nodejs.org/api/async_context.html"},"AsyncLocal")," for this task"),(0,i.kt)("h3",{id:"hardened-dockerfile"},"Hardened .dockerfile"),(0,i.kt)("p",null,"Although a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from ",(0,i.kt)("a",{parentName:"p",href:"https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/"},"this article")," and already apply 90% of the guidelines"),(0,i.kt)("h3",{id:"additional-orm-option-prisma"},"Additional ORM option: Prisma"),(0,i.kt)("p",null,"Prisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma"),(0,i.kt)("p",null,"Why did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this ",(0,i.kt)("a",{parentName:"p",href:"https://practica.dev/blog/is-prisma-better-than-your-traditional-orm/"},"blog post")),(0,i.kt)("h3",{id:"many-small-enhancements"},"Many small enhancements"),(0,i.kt)("p",null,"More than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more"),(0,i.kt)("h2",{id:"where-do-i-start"},"Where do I start?"),(0,i.kt)("p",null,"Definitely follow the ",(0,i.kt)("a",{parentName:"p",href:"https://practica.dev/the-basics/getting-started-quickly"},"getting started guide first")," and then read the guide ",(0,i.kt)("a",{parentName:"p",href:"https://practica.dev/the-basics/coding-with-practica"},"coding with practica")," to realize its full power and genuine value. We will be thankful to receive your feedback"))}u.isMDXComponent=!0}}]); \ No newline at end of file +"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[1867],{3905:(e,t,a)=>{a.d(t,{Zo:()=>p,kt:()=>m});var r=a(7294);function i(e,t,a){return t in e?Object.defineProperty(e,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):e[t]=a,e}function n(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,r)}return a}function o(e){for(var t=1;t=0||(i[a]=e[a]);return i}(e,t);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,a)&&(i[a]=e[a])}return i}var s=r.createContext({}),c=function(e){var t=r.useContext(s),a=t;return e&&(a="function"==typeof e?e(t):o(o({},t),e)),a},p=function(e){var t=c(e.components);return r.createElement(s.Provider,{value:t},e.children)},u={inlineCode:"code",wrapper:function(e){var t=e.children;return r.createElement(r.Fragment,{},t)}},d=r.forwardRef((function(e,t){var a=e.components,i=e.mdxType,n=e.originalType,s=e.parentName,p=l(e,["components","mdxType","originalType","parentName"]),d=c(a),m=i,h=d["".concat(s,".").concat(m)]||d[m]||u[m]||n;return a?r.createElement(h,o(o({ref:t},p),{},{components:a})):r.createElement(h,o({ref:t},p))}));function m(e,t){var a=arguments,i=t&&t.mdxType;if("string"==typeof e||i){var n=a.length,o=new Array(n);o[0]=d;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l.mdxType="string"==typeof e?e:i,o[1]=l;for(var c=2;c{a.r(t),a.d(t,{assets:()=>s,contentTitle:()=>o,default:()=>u,frontMatter:()=>n,metadata:()=>l,toc:()=>c});var r=a(7462),i=(a(7294),a(3905));const n={slug:"practica-v0.0.6-is-alive",date:"2022-12-10T10:00",hide_table_of_contents:!0,title:"Practica v0.0.6 is alive",authors:["goldbergyoni","razluvaton","danielgluskin","michaelsalomon"],tags:["node.js","express","practica","prisma"]},o=void 0,l={permalink:"/blog/practica-v0.0.6-is-alive",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/v0.6-is-alive/index.md",source:"@site/blog/v0.6-is-alive/index.md",title:"Practica v0.0.6 is alive",description:"Where is our focus now?",date:"2022-12-10T10:00:00.000Z",formattedDate:"December 10, 2022",tags:[{label:"node.js",permalink:"/blog/tags/node-js"},{label:"express",permalink:"/blog/tags/express"},{label:"practica",permalink:"/blog/tags/practica"},{label:"prisma",permalink:"/blog/tags/prisma"}],readingTime:1.47,hasTruncateMarker:!1,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Raz Luvaton",title:"Practica.js core maintainer",url:"https://github.com/rluvaton",imageURL:"https://avatars.githubusercontent.com/u/16746759?v=4",key:"razluvaton"},{name:"Daniel Gluskin",title:"Practica.js core maintainer",url:"https://github.com/DanielGluskin",imageURL:"https://avatars.githubusercontent.com/u/17989958?v=4",key:"danielgluskin"},{name:"Michael Salomon",title:"Practica.js core maintainer",url:"https://github.com/mikicho",imageURL:"https://avatars.githubusercontent.com/u/11459632?v=4",key:"michaelsalomon"}],frontMatter:{slug:"practica-v0.0.6-is-alive",date:"2022-12-10T10:00",hide_table_of_contents:!0,title:"Practica v0.0.6 is alive",authors:["goldbergyoni","razluvaton","danielgluskin","michaelsalomon"],tags:["node.js","express","practica","prisma"]},prevItem:{title:"Testing the dark scenarios of your Node.js application",permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application"},nextItem:{title:"Is Prisma better than your 'traditional' ORM?",permalink:"/blog/is-prisma-better-than-your-traditional-orm"}},s={authorsImageUrls:[void 0,void 0,void 0,void 0]},c=[{value:"Where is our focus now?",id:"where-is-our-focus-now",level:2},{value:"What's new?",id:"whats-new",level:2},{value:"Request-level store",id:"request-level-store",level:3},{value:"Hardened .dockerfile",id:"hardened-dockerfile",level:3},{value:"Additional ORM option: Prisma",id:"additional-orm-option-prisma",level:3},{value:"Many small enhancements",id:"many-small-enhancements",level:3},{value:"Where do I start?",id:"where-do-i-start",level:2}],p={toc:c};function u(e){let{components:t,...a}=e;return(0,i.kt)("wrapper",(0,r.Z)({},p,a,{components:t,mdxType:"MDXLayout"}),(0,i.kt)("h2",{id:"where-is-our-focus-now"},"Where is our focus now?"),(0,i.kt)("p",null,"We work in two parallel paths: enriching the supported best practices to make the code more production ready and at the same time enhance the existing code based off the community feedback"),(0,i.kt)("h2",{id:"whats-new"},"What's new?"),(0,i.kt)("h3",{id:"request-level-store"},"Request-level store"),(0,i.kt)("p",null,"Every request now has its own store of variables, you may assign information on the request-level so every code which was called from this specific request has access to these variables. For example, for storing the user permissions. One special variable that is stored is 'request-id' which is a unique UUID per request (also called correlation-id). The logger automatically will emit this to every log entry. We use the built-in ",(0,i.kt)("a",{parentName:"p",href:"https://nodejs.org/api/async_context.html"},"AsyncLocal")," for this task"),(0,i.kt)("h3",{id:"hardened-dockerfile"},"Hardened .dockerfile"),(0,i.kt)("p",null,"Although a Dockerfile may contain 10 lines, it easy and common to include 20 mistakes in these short artifact. For example, commonly npmrc secrets are leaked, usage of vulnerable base image and other typical mistakes. Our .Dockerfile follows the best practices from ",(0,i.kt)("a",{parentName:"p",href:"https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/"},"this article")," and already apply 90% of the guidelines"),(0,i.kt)("h3",{id:"additional-orm-option-prisma"},"Additional ORM option: Prisma"),(0,i.kt)("p",null,"Prisma is an emerging ORM with great type safe support and awesome DX. We will keep Sequelize as our default ORM while Prisma will be an optional choice using the flag: --orm=prisma"),(0,i.kt)("p",null,"Why did we add it to our tools basket and why Sequelize is still the default? We summarized all of our thoughts and data in this ",(0,i.kt)("a",{parentName:"p",href:"https://practica.dev/blog/is-prisma-better-than-your-traditional-orm/"},"blog post")),(0,i.kt)("h3",{id:"many-small-enhancements"},"Many small enhancements"),(0,i.kt)("p",null,"More than 10 PR were merged with CLI experience improvements, bug fixes, code patterns enhancements and more"),(0,i.kt)("h2",{id:"where-do-i-start"},"Where do I start?"),(0,i.kt)("p",null,"Definitely follow the ",(0,i.kt)("a",{parentName:"p",href:"https://practica.dev/the-basics/getting-started-quickly"},"getting started guide first")," and then read the guide ",(0,i.kt)("a",{parentName:"p",href:"https://practica.dev/the-basics/coding-with-practica"},"coding with practica")," to realize its full power and genuine value. We will be thankful to receive your feedback"))}u.isMDXComponent=!0}}]); \ No newline at end of file diff --git a/assets/js/e25a597f.b2cc51c1.js b/assets/js/e25a597f.b2cc51c1.js deleted file mode 100644 index 0cc71242..00000000 --- a/assets/js/e25a597f.b2cc51c1.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[9768],{3905:(e,t,o)=>{o.d(t,{Zo:()=>c,kt:()=>u});var n=o(7294);function a(e,t,o){return t in e?Object.defineProperty(e,t,{value:o,enumerable:!0,configurable:!0,writable:!0}):e[t]=o,e}function i(e,t){var o=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),o.push.apply(o,n)}return o}function r(e){for(var t=1;t=0||(a[o]=e[o]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,o)&&(a[o]=e[o])}return a}var l=n.createContext({}),h=function(e){var t=n.useContext(l),o=t;return e&&(o="function"==typeof e?e(t):r(r({},t),e)),o},c=function(e){var t=h(e.components);return n.createElement(l.Provider,{value:t},e.children)},p={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},d=n.forwardRef((function(e,t){var o=e.components,a=e.mdxType,i=e.originalType,l=e.parentName,c=s(e,["components","mdxType","originalType","parentName"]),d=h(o),u=a,m=d["".concat(l,".").concat(u)]||d[u]||p[u]||i;return o?n.createElement(m,r(r({ref:t},c),{},{components:o})):n.createElement(m,r({ref:t},c))}));function u(e,t){var o=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=o.length,r=new Array(i);r[0]=d;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s.mdxType="string"==typeof e?e:a,r[1]=s;for(var h=2;h{o.r(t),o.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>p,frontMatter:()=>i,metadata:()=>s,toc:()=>h});var n=o(7462),a=(o(7294),o(3905));const i={slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},r="Which Monorepo is right for a Node.js BACKEND\xa0now?",s={permalink:"/blog/monorepo-backend",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/which-monorepo/index.md",source:"@site/blog/which-monorepo/index.md",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",description:"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling",date:"2023-07-07T05:38:19.000Z",formattedDate:"July 7, 2023",tags:[{label:"monorepo",permalink:"/blog/tags/monorepo"},{label:"decisions",permalink:"/blog/tags/decisions"}],readingTime:16.925,hasTruncateMarker:!0,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Michael Salomon",title:"Practica.js core maintainer",url:"https://github.com/mikicho",imageURL:"https://avatars.githubusercontent.com/u/11459632?v=4",key:"michaelsalomon"}],frontMatter:{slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},prevItem:{title:"Testing the dark scenarios of your Node.js application",permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application"},nextItem:{title:"Practica v0.0.6 is alive",permalink:"/blog/practica-v0.0.6-is-alive"}},l={authorsImageUrls:[void 0,void 0]},h=[{value:"What are we looking\xa0at",id:"what-are-we-lookingat",level:2}],c={toc:h};function p(e){let{components:t,...i}=e;return(0,a.kt)("wrapper",(0,n.Z)({},c,i,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("p",null,"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/practicajs/practica"},"Practica.js"),". In this post, we'd like to share our considerations in choosing our monorepo tooling"),(0,a.kt)("p",null,(0,a.kt)("img",{alt:"Monorepos",src:o(1377).Z,width:"1400",height:"796"})),(0,a.kt)("h2",{id:"what-are-we-lookingat"},"What are we looking\xa0at"),(0,a.kt)("p",null,"The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries \u2014 ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/lerna/lerna/issues/2703"},"Lerna- has just retired.")," When looking closely, it might not be just a coincidence \u2014 With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused \u2014 What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you\u2019re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow."),(0,a.kt)("p",null,"This post is concerned with backend-only and Node.js. It also scoped to ",(0,a.kt)("em",{parentName:"p"},"typical")," business solutions. If you\u2019re Google/FB developer who is faced with 8,000 packages \u2014 sorry, you need special gear. Consequently, monster Monorepo tooling like ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/thundergolfer/example-bazel-monorepo"},"Bazel")," is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it\u2019s not actually maintained anymore \u2014 it\u2019s a good baseline for comparison)."),(0,a.kt)("p",null,"Let\u2019s start? When human beings use the term Monorepo, they typically refer to one or more of the following ",(0,a.kt)("em",{parentName:"p"},"4 layers below.")," Each one of them can bring value to your project, each has different consequences, tooling, and features:"),(0,a.kt)("h1",{id:"layer-1-plain-old-folders-to-stay-on-top-of-your-code"},"Layer 1: Plain old folders to stay on top of your code"),(0,a.kt)("p",null,"With zero tooling and only by having all the Microservice and libraries together in the same root folder, a developer gets great management perks and tons of value: Navigation, search across components, deleting a library instantly, debugging, ",(0,a.kt)("em",{parentName:"p"},"quickly")," adding new components. Consider the alternative with multi-repo approach \u2014 adding a new component for modularity demands opening and configuring a new GitHub repository. Not just a hassle but also greater chances of developers choosing the short path and including the new code in some semi-relevant existing package. In plain words, zero-tooling Monorepos can increase modularity."),(0,a.kt)("p",null,"This layer is often overlooked. If your codebase is not huge and the components are highly decoupled (more on this later)\u2014 it might be all you need. We\u2019ve seen a handful of successful Monorepo solutions without any special tooling."),(0,a.kt)("p",null,"With that said, some of the newer tools augment this experience with interesting features:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"Both ",(0,a.kt)("a",{parentName:"li",href:"https://turborepo.org/"},"Turborepo")," and ",(0,a.kt)("a",{parentName:"li",href:"https://nx.dev/structure/dependency-graph"},"Nx")," and also ",(0,a.kt)("a",{parentName:"li",href:"https://www.npmjs.com/package/lerna-dependency-graph"},"Lerna")," provide a visual representation of the packages\u2019 dependencies"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://nx.dev/structure/monorepo-tags"},"Nx allows \u2018visibility rules\u2019")," which is about enforcing who can use what. Consider, a \u2018checkout\u2019 library that should be approached only by the \u2018order Microservice\u2019 \u2014 deviating from this will result in failure during development (not runtime enforcement)")),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/0*pHZKRlGT6iOKCmzg.jpg",alt:null})),(0,a.kt)("p",null,"Nx dependencies graph"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://nx.dev/generators/workspace-generators"},"Nx workspace generator")," allows scaffolding out components. Whenever a team member needs to craft a new controller/library/class/Microservice, she just invokes a CLI command which products code based on a community or organization template. This enforces consistency and best practices sharing")),(0,a.kt)("h1",{id:"layer-2-tasks-and-pipeline-to-build-your-code-efficiently"},"Layer 2: Tasks and pipeline to build your code efficiently"),(0,a.kt)("p",null,"Even in a world of autonomous components, there are management tasks that must be applied in a batch like applying a security patch via npm update, running the tests of ",(0,a.kt)("em",{parentName:"p"},"multiple")," components that were affected by a change, publish 3 related libraries to name a few examples. All Monorepo tools support this basic functionality of invoking some command over a group of packages. For example, Lerna, Nx, and Turborepo do."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*wu7xtN97-Ihz4uCSDwd0mA.png",alt:null})),(0,a.kt)("p",null,"Apply some commands over multiple packages"),(0,a.kt)("p",null,"In some projects, invoking a cascading command is all you need. Mostly if each package has an autonomous life cycle and the build process spans a single package (more on this later). In some other types of projects where the workflow demands testing/running and publishing/deploying many packages together \u2014 this will end in a terribly slow experience. Consider a solution with hundred of packages that are transpiled and bundled \u2014 one might wait minutes for a wide test to run. While it\u2019s not always a great practice to rely on wide/E2E tests, it\u2019s quite common in the wild. This is exactly where the new wave of Monorepo tooling shines \u2014 ",(0,a.kt)("em",{parentName:"p"},"deeply")," optimizing the build process. I should say this out loud: These tools bring beautiful and innovative build optimizations:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Parallelization \u2014")," If two commands or packages are orthogonal to each other, the commands will run in two different threads or processes. Typically your quality control involves testing, lining, license checking, CVE checking \u2014 why not parallelize?"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Smart execution plan \u2014"),"Beyond parallelization, the optimized tasks execution order is determined based on many factors. Consider a build that includes A, B, C where A, C depend on B \u2014 naively, a build system would wait for B to build and only then run A & C. This can be optimized if we run A & C\u2019s ",(0,a.kt)("em",{parentName:"li"},"isolated")," unit tests ",(0,a.kt)("em",{parentName:"li"},"while")," building B and not afterward. By running task in parallel as early as possible, the overall execution time is improved \u2014 this has a remarkable impact mostly when hosting a high number of components. See below a visualization example of a pipeline improvement")),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/0*C6cxCblQU8ckTIQk.png",alt:null})),(0,a.kt)("p",null,"A modern tool advantage over old Lerna. Taken from Turborepo website"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Detect who is affected by a change \u2014")," Even on a system with high coupling between packages, it\u2019s usually not necessary to run ",(0,a.kt)("em",{parentName:"li"},"all")," packages rather than only those who are affected by a change. What exactly is \u2018affected\u2019? Packages/Microservices that depend upon another package that has changed. Some of the toolings can ignore minor changes that are unlikely to break others. This is not a great performance booster but also an amazing testing feature \u2014developers can get quick feedback on whether any of their clients were broken. Both Nx and Turborepo support this feature. Lerna can tell only which of the Monorepo package has changed"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Sub-systems (i.e., projects) \u2014")," Similarly to \u2018affected\u2019 above, modern tooling can realize portions of the graph that are inter-connected (a project or application) while others are not reachable by the component in context (another project) so they know to involve only packages of the relevant group"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Caching \u2014")," This is a serious speed booster: Nx and Turborepo cache the result/output of tasks and avoid running them again on consequent builds if unnecessary. For example, consider long-running tests of a Microservice, when commanding to re-build this Microservice, the tooling might realize that nothing has changed and the test will get skipped. This is achieved by generating a hashmap of all the dependent resources \u2014 if any of these resources haven\u2019t change, then the hashmap will be the same and the task will get skipped. They even cache the stdout of the command, so when you run a cached version it acts like the real thing \u2014 consider running 200 tests, seeing all the log statements of the tests, getting results over the terminal in 200 ms, everything acts like \u2018real testing while in fact, the tests did not run at all rather the cache!"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Remote caching \u2014")," Similarly to caching, only by placing the task\u2019s hashmaps and result on a global server so further executions on other team member\u2019s computers will also skip unnecessary tasks. In huge Monorepo projects that rely on E2E tests and must build all packages for development, this can save a great deal of time")),(0,a.kt)("h1",{id:"layer-3-hoist-your-dependencies-to-boost-npm-installation"},"Layer 3: Hoist your dependencies to boost npm installation"),(0,a.kt)("p",null,"The speed optimizations that were described above won\u2019t be of help if the bottleneck is the big bull of mud that is called \u2018npm install\u2019 (not to criticize, it\u2019s just hard by nature). Take a typical scenario as an example, given dozens of components that should be built, they could easily trigger the installation of thousands of sub-dependencies. Although they use quite similar dependencies (e.g., same logger, same ORM), if the dependency version is not equal then npm will duplicate (",(0,a.kt)("a",{parentName:"p",href:"https://rushjs.io/pages/advanced/npm_doppelgangers/"},"the NPM doppelgangers problem"),") the installation of those packages which might result in a long process."),(0,a.kt)("p",null,"This is where the workspace line of tools (e.g., Yarn workspace, npm workspaces, PNPM) kicks in and introduces some optimization \u2014 Instead of installing dependencies inside each component \u2018NODE_MODULES\u2019 folder, it will create one centralized folder and link all the dependencies over there. This can show a tremendous boost in install time for huge projects. On the other hand, if you always focus on one component at a time, installing the packages of a single Microservice/library should not be a concern."),(0,a.kt)("p",null,"Both Nx and Turborepo can rely on the package manager/workspace to provide this layer of optimizations. In other words, Nx and Turborepo are the layer above the package manager who take care of optimized dependencies installation."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*dhyCWSbzpIi5iagR4OB4zQ.png",alt:null})),(0,a.kt)("p",null,"On top of this, Nx introduces one more non-standard, maybe even controversial, technique: There might be only ONE package.json at the root folder of the entire Monorepo. By default, when creating components using Nx, they will not have their own package.json! Instead, all will share the root package.json. Going this way, all the Microservice/libraries share their dependencies and the installation time is improved. Note: It\u2019s possible to create \u2018publishable\u2019 components that do have a package.json, it\u2019s just not the default."),(0,a.kt)("p",null,"I\u2019m concerned here. Sharing dependencies among packages increases the coupling, what if Microservice1 wishes to bump dependency1 version but Microservice2 can\u2019t do this at the moment? Also, package.json is part of Node.js ",(0,a.kt)("em",{parentName:"p"},"runtime")," and excluding it from the component root loses important features like package.json main field or ESM exports (telling the clients which files are exposed). I ran some POC with Nx last week and found myself blocked \u2014 library B was wadded, I tried to import it from Library A but couldn\u2019t get the \u2018import\u2019 statement to specify the right package name. The natural action was to open B\u2019s package.json and check the name, but there is no Package.json\u2026 How do I determine its name? Nx docs are great, finally, I found the answer, but I had to spend time learning a new \u2018framework\u2019."),(0,a.kt)("h1",{id:"stop-for-a-second-its-all-about-your-workflow"},"Stop for a second: It\u2019s all about your workflow"),(0,a.kt)("p",null,"We deal with tooling and features, but it\u2019s actually meaningless evaluating these options before determining whether your preferred workflow is ",(0,a.kt)("em",{parentName:"p"},"synchronized or independent")," (we will discuss this in a few seconds)",(0,a.kt)("em",{parentName:"p"},".")," This upfront ",(0,a.kt)("em",{parentName:"p"},"fundamental")," decision will change almost everything."),(0,a.kt)("p",null,"Consider the following example with 3 components: Library 1 is introducing some major and breaking changes, Microservice1 and Microservice2 depend upon Library1 and should react to those breaking changes. How?"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option A \u2014 The synchronized workflow-")," Going with this development style, all the three components will be developed and deployed in one chunk ",(0,a.kt)("em",{parentName:"p"},"together"),". Practically, a developer will code the changes in Library1, test libray1 and also run wide integration/e2e tests that include Microservice1 and Microservice2. When they're ready, the version of all components will get bumped. Finally, they will get deployed ",(0,a.kt)("em",{parentName:"p"},"together.")),(0,a.kt)("p",null,"Going with this approach, the developer has the chance of seeing the full flow from the client's perspective (Microservice1 and 2), the tests cover not only the library but also through the eyes of the clients who actually use it. On the flip side, it mandates updating all the depend-upon components (could be dozens), doing so increases the risk\u2019s blast radius as more units are affected and should be considered before deployment. Also, working on a large unit of work demands building and testing more things which will slow the build."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option B \u2014 Independent workflow-")," This style is about working a unit by unit, one bite at a time, and deploy each component independently based on its personal business considerations and priority. This is how it goes: A developer makes the changes in Library1, they must be tested carefully in the scope of Library1. Once she is ready, the SemVer is bumped to a new major and the library is published to a package manager registry (e.g., npm). What about the client Microservices? Well, the team of Microservice2 is super-busy now with other priorities, and skip this update for now (the same thing as we all delay many of our npm updates,). However, Microservice1 is very much interested in this change \u2014 The team has to pro-actively update this dependency and grab the latest changes, run the tests and when they are ready, today or next week \u2014 deploy it."),(0,a.kt)("p",null,"Going with the independent workflow, the library author can move much faster because she does not need to take into account 2 or 30 other components \u2014 some are coded by different teams. This workflow also ",(0,a.kt)("em",{parentName:"p"},"forces her")," to write efficient tests against the library \u2014 it\u2019s her only safety net and is likely to end with autonomous components that have low coupling to others. On the other hand, testing in isolation without the client\u2019s perspective loses some dimension of realism. Also, if a single developer has to update 5 units \u2014 publishing each individually to the registry and then updating within all the dependencies can be a little tedious."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*eeJFL3_vo5tCrWvVY-surg.png",alt:null})),(0,a.kt)("p",null,"Synchronized and independent workflows illustrated"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"On the illusion of synchronicity")),(0,a.kt)("p",null,"In distributed systems, it\u2019s not feasible to achieve 100% synchronicity \u2014 believing otherwise can lead to design faults. Consider a breaking change in Microservice1, now its client Microservice2 is adapting and ready for the change. These two Microservices are deployed together but due to the nature of Microservices and distributed runtime (e.g., Kubernetes) the deployment of Microservice1 only fail. Now, Microservice2\u2019s code is not aligned with Microservice1 production and we are faced with a production bug. This line of failures can be handled to an extent also with a synchronized workflow \u2014 The deployment should orchestrate the rollout of each unit so each one is deployed at a time. Although this approach is doable, it increased the chances of large-scoped rollback and increases deployment fear."),(0,a.kt)("p",null,"This fundamental decision, synchronized or independent, will determine so many things \u2014 Whether performance is an issue or not at all (when working on a single unit), hoisting dependencies or leaving a dedicated node_modules in every package\u2019s folder, and whether to create a local link between packages which is described in the next paragraph."),(0,a.kt)("h1",{id:"layer-4-link-your-packages-for-immediate-feedback"},"Layer 4: Link your packages for immediate feedback"),(0,a.kt)("p",null,"When having a Monorepo, there is always the unavoidable dilemma of how to link between the components:"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option 1: Using npm \u2014")," Each library is a standard npm package and its client installs it via the standards npm commands. Given Microservice1 and Library1, this will end with two copies of Library1: the one inside Microservices1/NODE_MODULES (i.e., the local copy of the consuming Microservice), and the 2nd is the development folder where the team is coding Library1."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option2: Just a plain folder \u2014")," With this, Library1 is nothing but a logical module inside a folder that Microservice1,2,3 just locally imports. NPM is not involved here, it\u2019s just code in a dedicated folder. This is for example how Nest.js modules are represented."),(0,a.kt)("p",null,"With option 1, teams benefit from all the great merits of a package manager \u2014 SemVer(!), tooling, standards, etc. However, should one update Library1, the changes won\u2019t get reflected in Microservice1 since it is grabbing its copy from the npm registry and the changes were not published yet. This is a fundamental pain with Monorepo and package managers \u2014 one can\u2019t just code over multiple packages and test/run the changes."),(0,a.kt)("p",null,"With option 2, teams lose all the benefits of a package manager: Every change is propagated immediately to all of the consumers."),(0,a.kt)("p",null,"How do we bring the good from both worlds (presumably)? Using linking. Lerna, Nx, the various package manager workspaces (Yarn, npm, etc) allow using npm libraries and at the same time link between the clients (e.g., Microservice1) and the library. Under the hood, they created a symbolic link. In development mode, changes are propagated immediately, in deployment time \u2014 the copy is grabbed from the registry."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*9PkNrnbnibFdbvPieq-y9g.png",alt:null})),(0,a.kt)("p",null,"Linking packages in a Monorepo"),(0,a.kt)("p",null,"If you\u2019re doing the synchronized workflow, you\u2019re all set. Only now any risky change that is introduced by Library3, must be handled NOW by the 10 Microservices that consume it."),(0,a.kt)("p",null,"If favoring the independent workflow, this is of course a big concern. Some may call this direct linking style a \u2018monolith monorepo\u2019, or maybe a \u2018monolitho\u2019. However, when not linking, it\u2019s harder to debug a small issue between the Microservice and the npm library. What I typically do is ",(0,a.kt)("em",{parentName:"p"},"temporarily link")," (with npm link) between the packages",(0,a.kt)("em",{parentName:"p"},",")," debug, code, then finally remove the link."),(0,a.kt)("p",null,"Nx is taking a slightly more disruptive approach \u2014 it is using ",(0,a.kt)("a",{parentName:"p",href:"https://www.typescriptlang.org/tsconfig#paths"},"TypeScript paths")," to bind between the components. When Microservice1 is importing Library1, to avoid the full local path, it creates a TypeScript mapping between the library name and the full path. But wait a minute, there is no TypeScript in production so how could it work? Well, in serving/bundling time it webpacks and stitches the components together. Not a very standard way of doing Node.js work."),(0,a.kt)("h1",{id:"closing-what-should-you-use"},"Closing: What should you use?"),(0,a.kt)("p",null,"It\u2019s all about your workflow and architecture \u2014 a huge unseen cross-road stands in front of the Monorepo tooling decision."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Scenario A \u2014")," If your architecture dictates a ",(0,a.kt)("em",{parentName:"p"},"synchronized workflow")," where all packages are deployed together, or at least developed in collaboration \u2014 then there is a strong need for a rich tool to manage this coupling and boost the performance. In this case, Nx might be a great choice."),(0,a.kt)("p",null,"For example, if your Microservice must keep the same versioning, or if the team really small and the same people are updating all the components, or if your modularization is not based on package manager but rather on framework-own modules (e.g., Nest.js), if you\u2019re doing frontend where the components inherently are published together, or if your testing strategy relies on E2E mostly \u2014 for all of these cases and others, Nx is a tool that was built to enhance the experience of coding many ",(0,a.kt)("em",{parentName:"p"},"relatively")," coupled components together. It is a great a sugar coat over systems that are unavoidably big and linked."),(0,a.kt)("p",null,"If your system is not inherently big or meant to synchronize packages deployment, fancy Monorepo features might increase the coupling between components. The Monorepo pyramid above draws a line between basic features that provide value without coupling components while other layers come with an architectural price to consider. Sometimes climbing up toward the tip is worth the consequences, just make this decision consciously."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*c2qYYpVGG667bkum-gB-5Q.png",alt:null})),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Scenario B\u2014")," If you\u2019re into an ",(0,a.kt)("em",{parentName:"p"},"independent workflow")," where each package is developed, tested, and deployed (almost) independently \u2014 then inherently there is no need to fancy tools to orchestrate hundreds of packages. Most of the time there is just one package in focus. This calls for picking a leaner and simpler tool \u2014 Turborepo. By going this route, Monorepo is not something that affects your architecture, but rather a scoped tool for faster build execution. One specific tool that encourages an independent workflow is ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/giltayar/bilt"},"Bilt")," by Gil Tayar, it\u2019s yet to gain enough popularity but it might rise soon and is a great source to learn more about this philosophy of work."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"In any scenario, consider workspaces \u2014")," If you face performance issues that are caused by package installation, then the various workspace tools Yarn/npm/PNPM, can greatly minimize this overhead with a low footprint. That said, if you\u2019re working in an autonomous workflow, smaller are the chances of facing such issues. Don\u2019t just use tools unless there is a pain."),(0,a.kt)("p",null,"We tried to show the beauty of each and where it shines. If we\u2019re allowed to end this article with an opinionated choice: We greatly believe in an independent and autonomous workflow where the occasional developer of a package can code and deploy fearlessly without messing with dozens of other foreign packages. For this reason, Turborepo will be our favorite tool for the next season. We promise to tell you how it goes."),(0,a.kt)("h1",{id:"bonus-comparison-table"},"Bonus: Comparison table"),(0,a.kt)("p",null,"See below a detailed comparison table of the various tools and features:"),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*iHX_IdPW8XXXiZTyjFo6bw.png",alt:null})),(0,a.kt)("p",null,"Preview only, the complete table can be ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/practicajs/practica/blob/main/docs/docs/decisions/monorepo.md"},"found here")))}p.isMDXComponent=!0},1377:(e,t,o)=>{o.d(t,{Z:()=>n});const n=o.p+"assets/images/monorepo-high-level-291b29cc962144a43d78143889ba5d3b.png"}}]); \ No newline at end of file diff --git a/assets/js/e25a597f.b8006bb7.js b/assets/js/e25a597f.b8006bb7.js new file mode 100644 index 00000000..1822234c --- /dev/null +++ b/assets/js/e25a597f.b8006bb7.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[9768],{3905:(e,t,o)=>{o.d(t,{Zo:()=>c,kt:()=>u});var n=o(7294);function a(e,t,o){return t in e?Object.defineProperty(e,t,{value:o,enumerable:!0,configurable:!0,writable:!0}):e[t]=o,e}function i(e,t){var o=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),o.push.apply(o,n)}return o}function r(e){for(var t=1;t=0||(a[o]=e[o]);return a}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(n=0;n=0||Object.prototype.propertyIsEnumerable.call(e,o)&&(a[o]=e[o])}return a}var l=n.createContext({}),h=function(e){var t=n.useContext(l),o=t;return e&&(o="function"==typeof e?e(t):r(r({},t),e)),o},c=function(e){var t=h(e.components);return n.createElement(l.Provider,{value:t},e.children)},p={inlineCode:"code",wrapper:function(e){var t=e.children;return n.createElement(n.Fragment,{},t)}},d=n.forwardRef((function(e,t){var o=e.components,a=e.mdxType,i=e.originalType,l=e.parentName,c=s(e,["components","mdxType","originalType","parentName"]),d=h(o),u=a,m=d["".concat(l,".").concat(u)]||d[u]||p[u]||i;return o?n.createElement(m,r(r({ref:t},c),{},{components:o})):n.createElement(m,r({ref:t},c))}));function u(e,t){var o=arguments,a=t&&t.mdxType;if("string"==typeof e||a){var i=o.length,r=new Array(i);r[0]=d;var s={};for(var l in t)hasOwnProperty.call(t,l)&&(s[l]=t[l]);s.originalType=e,s.mdxType="string"==typeof e?e:a,r[1]=s;for(var h=2;h{o.r(t),o.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>p,frontMatter:()=>i,metadata:()=>s,toc:()=>h});var n=o(7462),a=(o(7294),o(3905));const i={slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},r="Which Monorepo is right for a Node.js BACKEND\xa0now?",s={permalink:"/blog/monorepo-backend",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/which-monorepo/index.md",source:"@site/blog/which-monorepo/index.md",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",description:"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in Practica.js. In this post, we'd like to share our considerations in choosing our monorepo tooling",date:"2023-07-11T13:19:18.000Z",formattedDate:"July 11, 2023",tags:[{label:"monorepo",permalink:"/blog/tags/monorepo"},{label:"decisions",permalink:"/blog/tags/decisions"}],readingTime:16.925,hasTruncateMarker:!0,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Michael Salomon",title:"Practica.js core maintainer",url:"https://github.com/mikicho",imageURL:"https://avatars.githubusercontent.com/u/11459632?v=4",key:"michaelsalomon"}],frontMatter:{slug:"monorepo-backend",title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",authors:["goldbergyoni","michaelsalomon"],tags:["monorepo","decisions"]},nextItem:{title:"Testing the dark scenarios of your Node.js application",permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application"}},l={authorsImageUrls:[void 0,void 0]},h=[{value:"What are we looking\xa0at",id:"what-are-we-lookingat",level:2}],c={toc:h};function p(e){let{components:t,...i}=e;return(0,a.kt)("wrapper",(0,n.Z)({},c,i,{components:t,mdxType:"MDXLayout"}),(0,a.kt)("p",null,"As a Node.js starter, choosing the right libraries and frameworks for our users is the bread and butter of our work in ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/practicajs/practica"},"Practica.js"),". In this post, we'd like to share our considerations in choosing our monorepo tooling"),(0,a.kt)("p",null,(0,a.kt)("img",{alt:"Monorepos",src:o(1377).Z,width:"1400",height:"796"})),(0,a.kt)("h2",{id:"what-are-we-lookingat"},"What are we looking\xa0at"),(0,a.kt)("p",null,"The Monorepo market is hot like fire. Weirdly, now when the demand for Monoreps is exploding, one of the leading libraries \u2014 ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/lerna/lerna/issues/2703"},"Lerna- has just retired.")," When looking closely, it might not be just a coincidence \u2014 With so many disruptive and shiny features brought on by new vendors, Lerna failed to keep up with the pace and stay relevant. This bloom of new tooling gets many confused \u2014 What is the right choice for my next project? What should I look at when choosing a Monorepo tool? This post is all about curating this information overload, covering the new tooling, emphasizing what is important, and finally share some recommendations. If you are here for tools and features, you\u2019re in the right place, although you might find yourself on a soul-searching journey to what is your desired development workflow."),(0,a.kt)("p",null,"This post is concerned with backend-only and Node.js. It also scoped to ",(0,a.kt)("em",{parentName:"p"},"typical")," business solutions. If you\u2019re Google/FB developer who is faced with 8,000 packages \u2014 sorry, you need special gear. Consequently, monster Monorepo tooling like ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/thundergolfer/example-bazel-monorepo"},"Bazel")," is left-out. We will cover here some of the most popular Monorepo tools including Turborepo, Nx, PNPM, Yarn/npm workspace, and Lerna (although it\u2019s not actually maintained anymore \u2014 it\u2019s a good baseline for comparison)."),(0,a.kt)("p",null,"Let\u2019s start? When human beings use the term Monorepo, they typically refer to one or more of the following ",(0,a.kt)("em",{parentName:"p"},"4 layers below.")," Each one of them can bring value to your project, each has different consequences, tooling, and features:"),(0,a.kt)("h1",{id:"layer-1-plain-old-folders-to-stay-on-top-of-your-code"},"Layer 1: Plain old folders to stay on top of your code"),(0,a.kt)("p",null,"With zero tooling and only by having all the Microservice and libraries together in the same root folder, a developer gets great management perks and tons of value: Navigation, search across components, deleting a library instantly, debugging, ",(0,a.kt)("em",{parentName:"p"},"quickly")," adding new components. Consider the alternative with multi-repo approach \u2014 adding a new component for modularity demands opening and configuring a new GitHub repository. Not just a hassle but also greater chances of developers choosing the short path and including the new code in some semi-relevant existing package. In plain words, zero-tooling Monorepos can increase modularity."),(0,a.kt)("p",null,"This layer is often overlooked. If your codebase is not huge and the components are highly decoupled (more on this later)\u2014 it might be all you need. We\u2019ve seen a handful of successful Monorepo solutions without any special tooling."),(0,a.kt)("p",null,"With that said, some of the newer tools augment this experience with interesting features:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},"Both ",(0,a.kt)("a",{parentName:"li",href:"https://turborepo.org/"},"Turborepo")," and ",(0,a.kt)("a",{parentName:"li",href:"https://nx.dev/structure/dependency-graph"},"Nx")," and also ",(0,a.kt)("a",{parentName:"li",href:"https://www.npmjs.com/package/lerna-dependency-graph"},"Lerna")," provide a visual representation of the packages\u2019 dependencies"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://nx.dev/structure/monorepo-tags"},"Nx allows \u2018visibility rules\u2019")," which is about enforcing who can use what. Consider, a \u2018checkout\u2019 library that should be approached only by the \u2018order Microservice\u2019 \u2014 deviating from this will result in failure during development (not runtime enforcement)")),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/0*pHZKRlGT6iOKCmzg.jpg",alt:null})),(0,a.kt)("p",null,"Nx dependencies graph"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("a",{parentName:"li",href:"https://nx.dev/generators/workspace-generators"},"Nx workspace generator")," allows scaffolding out components. Whenever a team member needs to craft a new controller/library/class/Microservice, she just invokes a CLI command which products code based on a community or organization template. This enforces consistency and best practices sharing")),(0,a.kt)("h1",{id:"layer-2-tasks-and-pipeline-to-build-your-code-efficiently"},"Layer 2: Tasks and pipeline to build your code efficiently"),(0,a.kt)("p",null,"Even in a world of autonomous components, there are management tasks that must be applied in a batch like applying a security patch via npm update, running the tests of ",(0,a.kt)("em",{parentName:"p"},"multiple")," components that were affected by a change, publish 3 related libraries to name a few examples. All Monorepo tools support this basic functionality of invoking some command over a group of packages. For example, Lerna, Nx, and Turborepo do."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*wu7xtN97-Ihz4uCSDwd0mA.png",alt:null})),(0,a.kt)("p",null,"Apply some commands over multiple packages"),(0,a.kt)("p",null,"In some projects, invoking a cascading command is all you need. Mostly if each package has an autonomous life cycle and the build process spans a single package (more on this later). In some other types of projects where the workflow demands testing/running and publishing/deploying many packages together \u2014 this will end in a terribly slow experience. Consider a solution with hundred of packages that are transpiled and bundled \u2014 one might wait minutes for a wide test to run. While it\u2019s not always a great practice to rely on wide/E2E tests, it\u2019s quite common in the wild. This is exactly where the new wave of Monorepo tooling shines \u2014 ",(0,a.kt)("em",{parentName:"p"},"deeply")," optimizing the build process. I should say this out loud: These tools bring beautiful and innovative build optimizations:"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Parallelization \u2014")," If two commands or packages are orthogonal to each other, the commands will run in two different threads or processes. Typically your quality control involves testing, lining, license checking, CVE checking \u2014 why not parallelize?"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Smart execution plan \u2014"),"Beyond parallelization, the optimized tasks execution order is determined based on many factors. Consider a build that includes A, B, C where A, C depend on B \u2014 naively, a build system would wait for B to build and only then run A & C. This can be optimized if we run A & C\u2019s ",(0,a.kt)("em",{parentName:"li"},"isolated")," unit tests ",(0,a.kt)("em",{parentName:"li"},"while")," building B and not afterward. By running task in parallel as early as possible, the overall execution time is improved \u2014 this has a remarkable impact mostly when hosting a high number of components. See below a visualization example of a pipeline improvement")),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/0*C6cxCblQU8ckTIQk.png",alt:null})),(0,a.kt)("p",null,"A modern tool advantage over old Lerna. Taken from Turborepo website"),(0,a.kt)("ul",null,(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Detect who is affected by a change \u2014")," Even on a system with high coupling between packages, it\u2019s usually not necessary to run ",(0,a.kt)("em",{parentName:"li"},"all")," packages rather than only those who are affected by a change. What exactly is \u2018affected\u2019? Packages/Microservices that depend upon another package that has changed. Some of the toolings can ignore minor changes that are unlikely to break others. This is not a great performance booster but also an amazing testing feature \u2014developers can get quick feedback on whether any of their clients were broken. Both Nx and Turborepo support this feature. Lerna can tell only which of the Monorepo package has changed"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Sub-systems (i.e., projects) \u2014")," Similarly to \u2018affected\u2019 above, modern tooling can realize portions of the graph that are inter-connected (a project or application) while others are not reachable by the component in context (another project) so they know to involve only packages of the relevant group"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Caching \u2014")," This is a serious speed booster: Nx and Turborepo cache the result/output of tasks and avoid running them again on consequent builds if unnecessary. For example, consider long-running tests of a Microservice, when commanding to re-build this Microservice, the tooling might realize that nothing has changed and the test will get skipped. This is achieved by generating a hashmap of all the dependent resources \u2014 if any of these resources haven\u2019t change, then the hashmap will be the same and the task will get skipped. They even cache the stdout of the command, so when you run a cached version it acts like the real thing \u2014 consider running 200 tests, seeing all the log statements of the tests, getting results over the terminal in 200 ms, everything acts like \u2018real testing while in fact, the tests did not run at all rather the cache!"),(0,a.kt)("li",{parentName:"ul"},(0,a.kt)("strong",{parentName:"li"},"Remote caching \u2014")," Similarly to caching, only by placing the task\u2019s hashmaps and result on a global server so further executions on other team member\u2019s computers will also skip unnecessary tasks. In huge Monorepo projects that rely on E2E tests and must build all packages for development, this can save a great deal of time")),(0,a.kt)("h1",{id:"layer-3-hoist-your-dependencies-to-boost-npm-installation"},"Layer 3: Hoist your dependencies to boost npm installation"),(0,a.kt)("p",null,"The speed optimizations that were described above won\u2019t be of help if the bottleneck is the big bull of mud that is called \u2018npm install\u2019 (not to criticize, it\u2019s just hard by nature). Take a typical scenario as an example, given dozens of components that should be built, they could easily trigger the installation of thousands of sub-dependencies. Although they use quite similar dependencies (e.g., same logger, same ORM), if the dependency version is not equal then npm will duplicate (",(0,a.kt)("a",{parentName:"p",href:"https://rushjs.io/pages/advanced/npm_doppelgangers/"},"the NPM doppelgangers problem"),") the installation of those packages which might result in a long process."),(0,a.kt)("p",null,"This is where the workspace line of tools (e.g., Yarn workspace, npm workspaces, PNPM) kicks in and introduces some optimization \u2014 Instead of installing dependencies inside each component \u2018NODE_MODULES\u2019 folder, it will create one centralized folder and link all the dependencies over there. This can show a tremendous boost in install time for huge projects. On the other hand, if you always focus on one component at a time, installing the packages of a single Microservice/library should not be a concern."),(0,a.kt)("p",null,"Both Nx and Turborepo can rely on the package manager/workspace to provide this layer of optimizations. In other words, Nx and Turborepo are the layer above the package manager who take care of optimized dependencies installation."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*dhyCWSbzpIi5iagR4OB4zQ.png",alt:null})),(0,a.kt)("p",null,"On top of this, Nx introduces one more non-standard, maybe even controversial, technique: There might be only ONE package.json at the root folder of the entire Monorepo. By default, when creating components using Nx, they will not have their own package.json! Instead, all will share the root package.json. Going this way, all the Microservice/libraries share their dependencies and the installation time is improved. Note: It\u2019s possible to create \u2018publishable\u2019 components that do have a package.json, it\u2019s just not the default."),(0,a.kt)("p",null,"I\u2019m concerned here. Sharing dependencies among packages increases the coupling, what if Microservice1 wishes to bump dependency1 version but Microservice2 can\u2019t do this at the moment? Also, package.json is part of Node.js ",(0,a.kt)("em",{parentName:"p"},"runtime")," and excluding it from the component root loses important features like package.json main field or ESM exports (telling the clients which files are exposed). I ran some POC with Nx last week and found myself blocked \u2014 library B was wadded, I tried to import it from Library A but couldn\u2019t get the \u2018import\u2019 statement to specify the right package name. The natural action was to open B\u2019s package.json and check the name, but there is no Package.json\u2026 How do I determine its name? Nx docs are great, finally, I found the answer, but I had to spend time learning a new \u2018framework\u2019."),(0,a.kt)("h1",{id:"stop-for-a-second-its-all-about-your-workflow"},"Stop for a second: It\u2019s all about your workflow"),(0,a.kt)("p",null,"We deal with tooling and features, but it\u2019s actually meaningless evaluating these options before determining whether your preferred workflow is ",(0,a.kt)("em",{parentName:"p"},"synchronized or independent")," (we will discuss this in a few seconds)",(0,a.kt)("em",{parentName:"p"},".")," This upfront ",(0,a.kt)("em",{parentName:"p"},"fundamental")," decision will change almost everything."),(0,a.kt)("p",null,"Consider the following example with 3 components: Library 1 is introducing some major and breaking changes, Microservice1 and Microservice2 depend upon Library1 and should react to those breaking changes. How?"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option A \u2014 The synchronized workflow-")," Going with this development style, all the three components will be developed and deployed in one chunk ",(0,a.kt)("em",{parentName:"p"},"together"),". Practically, a developer will code the changes in Library1, test libray1 and also run wide integration/e2e tests that include Microservice1 and Microservice2. When they're ready, the version of all components will get bumped. Finally, they will get deployed ",(0,a.kt)("em",{parentName:"p"},"together.")),(0,a.kt)("p",null,"Going with this approach, the developer has the chance of seeing the full flow from the client's perspective (Microservice1 and 2), the tests cover not only the library but also through the eyes of the clients who actually use it. On the flip side, it mandates updating all the depend-upon components (could be dozens), doing so increases the risk\u2019s blast radius as more units are affected and should be considered before deployment. Also, working on a large unit of work demands building and testing more things which will slow the build."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option B \u2014 Independent workflow-")," This style is about working a unit by unit, one bite at a time, and deploy each component independently based on its personal business considerations and priority. This is how it goes: A developer makes the changes in Library1, they must be tested carefully in the scope of Library1. Once she is ready, the SemVer is bumped to a new major and the library is published to a package manager registry (e.g., npm). What about the client Microservices? Well, the team of Microservice2 is super-busy now with other priorities, and skip this update for now (the same thing as we all delay many of our npm updates,). However, Microservice1 is very much interested in this change \u2014 The team has to pro-actively update this dependency and grab the latest changes, run the tests and when they are ready, today or next week \u2014 deploy it."),(0,a.kt)("p",null,"Going with the independent workflow, the library author can move much faster because she does not need to take into account 2 or 30 other components \u2014 some are coded by different teams. This workflow also ",(0,a.kt)("em",{parentName:"p"},"forces her")," to write efficient tests against the library \u2014 it\u2019s her only safety net and is likely to end with autonomous components that have low coupling to others. On the other hand, testing in isolation without the client\u2019s perspective loses some dimension of realism. Also, if a single developer has to update 5 units \u2014 publishing each individually to the registry and then updating within all the dependencies can be a little tedious."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*eeJFL3_vo5tCrWvVY-surg.png",alt:null})),(0,a.kt)("p",null,"Synchronized and independent workflows illustrated"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"On the illusion of synchronicity")),(0,a.kt)("p",null,"In distributed systems, it\u2019s not feasible to achieve 100% synchronicity \u2014 believing otherwise can lead to design faults. Consider a breaking change in Microservice1, now its client Microservice2 is adapting and ready for the change. These two Microservices are deployed together but due to the nature of Microservices and distributed runtime (e.g., Kubernetes) the deployment of Microservice1 only fail. Now, Microservice2\u2019s code is not aligned with Microservice1 production and we are faced with a production bug. This line of failures can be handled to an extent also with a synchronized workflow \u2014 The deployment should orchestrate the rollout of each unit so each one is deployed at a time. Although this approach is doable, it increased the chances of large-scoped rollback and increases deployment fear."),(0,a.kt)("p",null,"This fundamental decision, synchronized or independent, will determine so many things \u2014 Whether performance is an issue or not at all (when working on a single unit), hoisting dependencies or leaving a dedicated node_modules in every package\u2019s folder, and whether to create a local link between packages which is described in the next paragraph."),(0,a.kt)("h1",{id:"layer-4-link-your-packages-for-immediate-feedback"},"Layer 4: Link your packages for immediate feedback"),(0,a.kt)("p",null,"When having a Monorepo, there is always the unavoidable dilemma of how to link between the components:"),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option 1: Using npm \u2014")," Each library is a standard npm package and its client installs it via the standards npm commands. Given Microservice1 and Library1, this will end with two copies of Library1: the one inside Microservices1/NODE_MODULES (i.e., the local copy of the consuming Microservice), and the 2nd is the development folder where the team is coding Library1."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Option2: Just a plain folder \u2014")," With this, Library1 is nothing but a logical module inside a folder that Microservice1,2,3 just locally imports. NPM is not involved here, it\u2019s just code in a dedicated folder. This is for example how Nest.js modules are represented."),(0,a.kt)("p",null,"With option 1, teams benefit from all the great merits of a package manager \u2014 SemVer(!), tooling, standards, etc. However, should one update Library1, the changes won\u2019t get reflected in Microservice1 since it is grabbing its copy from the npm registry and the changes were not published yet. This is a fundamental pain with Monorepo and package managers \u2014 one can\u2019t just code over multiple packages and test/run the changes."),(0,a.kt)("p",null,"With option 2, teams lose all the benefits of a package manager: Every change is propagated immediately to all of the consumers."),(0,a.kt)("p",null,"How do we bring the good from both worlds (presumably)? Using linking. Lerna, Nx, the various package manager workspaces (Yarn, npm, etc) allow using npm libraries and at the same time link between the clients (e.g., Microservice1) and the library. Under the hood, they created a symbolic link. In development mode, changes are propagated immediately, in deployment time \u2014 the copy is grabbed from the registry."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*9PkNrnbnibFdbvPieq-y9g.png",alt:null})),(0,a.kt)("p",null,"Linking packages in a Monorepo"),(0,a.kt)("p",null,"If you\u2019re doing the synchronized workflow, you\u2019re all set. Only now any risky change that is introduced by Library3, must be handled NOW by the 10 Microservices that consume it."),(0,a.kt)("p",null,"If favoring the independent workflow, this is of course a big concern. Some may call this direct linking style a \u2018monolith monorepo\u2019, or maybe a \u2018monolitho\u2019. However, when not linking, it\u2019s harder to debug a small issue between the Microservice and the npm library. What I typically do is ",(0,a.kt)("em",{parentName:"p"},"temporarily link")," (with npm link) between the packages",(0,a.kt)("em",{parentName:"p"},",")," debug, code, then finally remove the link."),(0,a.kt)("p",null,"Nx is taking a slightly more disruptive approach \u2014 it is using ",(0,a.kt)("a",{parentName:"p",href:"https://www.typescriptlang.org/tsconfig#paths"},"TypeScript paths")," to bind between the components. When Microservice1 is importing Library1, to avoid the full local path, it creates a TypeScript mapping between the library name and the full path. But wait a minute, there is no TypeScript in production so how could it work? Well, in serving/bundling time it webpacks and stitches the components together. Not a very standard way of doing Node.js work."),(0,a.kt)("h1",{id:"closing-what-should-you-use"},"Closing: What should you use?"),(0,a.kt)("p",null,"It\u2019s all about your workflow and architecture \u2014 a huge unseen cross-road stands in front of the Monorepo tooling decision."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Scenario A \u2014")," If your architecture dictates a ",(0,a.kt)("em",{parentName:"p"},"synchronized workflow")," where all packages are deployed together, or at least developed in collaboration \u2014 then there is a strong need for a rich tool to manage this coupling and boost the performance. In this case, Nx might be a great choice."),(0,a.kt)("p",null,"For example, if your Microservice must keep the same versioning, or if the team really small and the same people are updating all the components, or if your modularization is not based on package manager but rather on framework-own modules (e.g., Nest.js), if you\u2019re doing frontend where the components inherently are published together, or if your testing strategy relies on E2E mostly \u2014 for all of these cases and others, Nx is a tool that was built to enhance the experience of coding many ",(0,a.kt)("em",{parentName:"p"},"relatively")," coupled components together. It is a great a sugar coat over systems that are unavoidably big and linked."),(0,a.kt)("p",null,"If your system is not inherently big or meant to synchronize packages deployment, fancy Monorepo features might increase the coupling between components. The Monorepo pyramid above draws a line between basic features that provide value without coupling components while other layers come with an architectural price to consider. Sometimes climbing up toward the tip is worth the consequences, just make this decision consciously."),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*c2qYYpVGG667bkum-gB-5Q.png",alt:null})),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"Scenario B\u2014")," If you\u2019re into an ",(0,a.kt)("em",{parentName:"p"},"independent workflow")," where each package is developed, tested, and deployed (almost) independently \u2014 then inherently there is no need to fancy tools to orchestrate hundreds of packages. Most of the time there is just one package in focus. This calls for picking a leaner and simpler tool \u2014 Turborepo. By going this route, Monorepo is not something that affects your architecture, but rather a scoped tool for faster build execution. One specific tool that encourages an independent workflow is ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/giltayar/bilt"},"Bilt")," by Gil Tayar, it\u2019s yet to gain enough popularity but it might rise soon and is a great source to learn more about this philosophy of work."),(0,a.kt)("p",null,(0,a.kt)("strong",{parentName:"p"},"In any scenario, consider workspaces \u2014")," If you face performance issues that are caused by package installation, then the various workspace tools Yarn/npm/PNPM, can greatly minimize this overhead with a low footprint. That said, if you\u2019re working in an autonomous workflow, smaller are the chances of facing such issues. Don\u2019t just use tools unless there is a pain."),(0,a.kt)("p",null,"We tried to show the beauty of each and where it shines. If we\u2019re allowed to end this article with an opinionated choice: We greatly believe in an independent and autonomous workflow where the occasional developer of a package can code and deploy fearlessly without messing with dozens of other foreign packages. For this reason, Turborepo will be our favorite tool for the next season. We promise to tell you how it goes."),(0,a.kt)("h1",{id:"bonus-comparison-table"},"Bonus: Comparison table"),(0,a.kt)("p",null,"See below a detailed comparison table of the various tools and features:"),(0,a.kt)("p",null,(0,a.kt)("img",{parentName:"p",src:"https://miro.medium.com/max/1400/1*iHX_IdPW8XXXiZTyjFo6bw.png",alt:null})),(0,a.kt)("p",null,"Preview only, the complete table can be ",(0,a.kt)("a",{parentName:"p",href:"https://github.com/practicajs/practica/blob/main/docs/docs/decisions/monorepo.md"},"found here")))}p.isMDXComponent=!0},1377:(e,t,o)=>{o.d(t,{Z:()=>n});const n=o.p+"assets/images/monorepo-high-level-291b29cc962144a43d78143889ba5d3b.png"}}]); \ No newline at end of file diff --git a/assets/js/ea907698.56ece2e6.js b/assets/js/ea907698.56ece2e6.js new file mode 100644 index 00000000..6c11172e --- /dev/null +++ b/assets/js/ea907698.56ece2e6.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[9362],{3905:(e,t,n)=>{n.d(t,{Zo:()=>h,kt:()=>p});var a=n(7294);function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function r(e){for(var t=1;t=0||(s[n]=e[n]);return s}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}var l=a.createContext({}),c=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},h=function(e){var t=c(e.components);return a.createElement(l.Provider,{value:t},e.children)},u={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},d=a.forwardRef((function(e,t){var n=e.components,s=e.mdxType,o=e.originalType,l=e.parentName,h=i(e,["components","mdxType","originalType","parentName"]),d=c(n),p=s,m=d["".concat(l,".").concat(p)]||d[p]||u[p]||o;return n?a.createElement(m,r(r({ref:t},h),{},{components:n})):a.createElement(m,r({ref:t},h))}));function p(e,t){var n=arguments,s=t&&t.mdxType;if("string"==typeof e||s){var o=n.length,r=new Array(o);r[0]=d;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i.mdxType="string"==typeof e?e:s,r[1]=i;for(var c=2;c{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>u,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var a=n(7462),s=(n(7294),n(3905));const o={slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},r=void 0,i={permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/crucial-tests/index.md",source:"@site/blog/crucial-tests/index.md",title:"Testing the dark scenarios of your Node.js application",description:"Where the dead-bodies are covered",date:"2023-07-07T11:00:00.000Z",formattedDate:"July 7, 2023",tags:[{label:"node.js",permalink:"/blog/tags/node-js"},{label:"testing",permalink:"/blog/tags/testing"},{label:"component-test",permalink:"/blog/tags/component-test"},{label:"fastify",permalink:"/blog/tags/fastify"},{label:"unit-test",permalink:"/blog/tags/unit-test"},{label:"integration",permalink:"/blog/tags/integration"},{label:"nock",permalink:"/blog/tags/nock"}],readingTime:20.3,hasTruncateMarker:!1,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Raz Luvaton",title:"Practica.js core maintainer",url:"https://github.com/rluvaton",imageURL:"https://avatars.githubusercontent.com/u/16746759?v=4",key:"razluvaton"}],frontMatter:{slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},prevItem:{title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",permalink:"/blog/monorepo-backend"},nextItem:{title:"Practica v0.0.6 is alive",permalink:"/blog/practica-v0.0.6-is-alive"}},l={authorsImageUrls:[void 0,void 0]},c=[{value:"Where the dead-bodies are covered",id:"where-the-dead-bodies-are-covered",level:2},{value:"Test Examples",id:"test-examples",level:2},{value:"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test",id:"\ufe0f-the-zombie-process-test",level:2},{value:"\ud83d\udc40 The observability test",id:"-the-observability-test",level:2},{value:"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code",id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code",level:2},{value:"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all",id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all",level:2},{value:"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much",id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much",level:2},{value:"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out",id:"-the-slow-collaborator-test---when-the-other-http-service-times-out",level:2},{value:"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation",id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation",level:2},{value:"\ud83d\udce6 Test the package as a consumer",id:"-test-the-package-as-a-consumer",level:2},{value:"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug",id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug",level:2},{value:"Even more ideas",id:"even-more-ideas",level:2},{value:"It's not just ideas, it a whole new mindset",id:"its-not-just-ideas-it-a-whole-new-mindset",level:2}],h={toc:c};function u(e){let{components:t,...o}=e;return(0,s.kt)("wrapper",(0,a.Z)({},h,o,{components:t,mdxType:"MDXLayout"}),(0,s.kt)("h2",{id:"where-the-dead-bodies-are-covered"},"Where the dead-bodies are covered"),(0,s.kt)("p",null,"This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked"),(0,s.kt)("p",null,"Some context first: How do we test a modern backend? With ",(0,s.kt)("a",{parentName:"p",href:"https://ritesh-kapoor.medium.com/testing-automation-what-are-pyramids-and-diamonds-67494fec7c55"},"the testing diamond"),", of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices"},"a guide with 50 best practices for integration tests in Node.js")),(0,s.kt)("p",null,"But there is a pitfall: most developers write ",(0,s.kt)("em",{parentName:"p"},"only")," semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime"),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"The hidden corners",src:n(8933).Z,width:"900",height:"521"})),(0,s.kt)("p",null,"Here are a handful of examples that might open your mind to a whole new class of risks and tests"),(0,s.kt)("p",null,(0,s.kt)("em",{parentName:"p"},"Side note: I've just released a comprehensive testing course that I've been working on for two years. \ud83c\udf81 It's now on sale, but only for the month of July. Check it out at ",(0,s.kt)("a",{parentName:"em",href:"https://testjavascript.com/"},"testjavascript.com"))),(0,s.kt)("h2",{id:"test-examples"},(0,s.kt)("strong",{parentName:"h2"},"Test Examples")),(0,s.kt)("h2",{id:"\ufe0f-the-zombie-process-test"},"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what? -")," In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see ",(0,s.kt)("a",{parentName:"p",href:"https://komodor.com/learn/kubernetes-readiness-probes-a-practical-guide/#:~:text=A%20readiness%20probe%20allows%20Kubernetes,on%20deletion%20of%20a%20pod."},"readiness probe"),"). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, api.js:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// A common express server initialization\nconst startWebServer = () => {\n return new Promise((resolve, reject) => {\n try {\n // A typical Express setup\n expressApp = express();\n defineRoutes(expressApp); // a function that defines all routes\n expressApp.listen(process.env.WEB_SERVER_PORT);\n } catch (error) {\n //log here, fire a metric, maybe even retry and finally:\n process.exit();\n }\n });\n};\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function\nconst sinon = require('sinon'); // a mocking library\n\ntest('When an error happens during the startup phase, then the process exits', async () => {\n // Arrange\n const processExitListener = sinon.stub(process, 'exit');\n // \ud83d\udc47 Choose a function that is part of the initialization phase and make it fail\n sinon\n .stub(routes, 'defineRoutes')\n .throws(new Error('Cant initialize connection'));\n\n // Act\n await api.startWebServer();\n\n // Assert\n expect(processExitListener.called).toBe(true);\n});\n")),(0,s.kt)("h2",{id:"-the-observability-test"},"\ud83d\udc40 The observability test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error ",(0,s.kt)("strong",{parentName:"p"},"correctly observable"),". In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, ",(0,s.kt)("em",{parentName:"p"},"including stack trace"),", cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When exception is throw during request, Then logger reports the mandatory fields', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n status: 'approved',\n };\n const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');\n sinon\n .stub(OrderRepository.prototype, 'addOrder')\n .rejects(new AppError('saving-failed', 'Order could not be saved', 500));\n const loggerDouble = sinon.stub(logger, 'error');\n\n //Act\n await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n expect(loggerDouble).toHaveBeenCalledWith({\n name: 'saving-failed',\n status: 500,\n stack: expect.any(String),\n message: expect.any(String),\n });\n expect(\n metricsExporterDouble).toHaveBeenCalledWith('error', {\n errorName: 'example-error',\n })\n});\n")),(0,s.kt)("h2",{id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code"},"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, ",(0,s.kt)("strong",{parentName:"p"},"hopefully if your code subscribed"),". How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:"),(0,s.kt)("p",null,"researches says that, rejection"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {\n //Arrange\n const loggerDouble = sinon.stub(logger, 'error');\n const processExitListener = sinon.stub(process, 'exit');\n const errorToThrow = new Error('An error that wont be caught \ud83d\ude33');\n\n //Act\n process.emit('uncaughtException', errorToThrow); //\ud83d\udc48 Where the magic is\n\n // Assert\n expect(processExitListener.called).toBe(false);\n expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);\n});\n")),(0,s.kt)("h2",{id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all"},"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n mode: 'draft',\n externalIdentifier: uuid(), //no existing record has this value\n };\n\n //Act\n const { status: addingHTTPStatus } = await axiosAPIClient.post(\n '/order',\n orderToAdd\n );\n\n //Assert\n const { status: fetchingHTTPStatus } = await axiosAPIClient.get(\n `/order/externalIdentifier/${orderToAdd.externalIdentifier}`\n ); // Trying to get the order that should have failed\n expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({\n addingHTTPStatus: 400,\n fetchingHTTPStatus: 404,\n });\n // \ud83d\udc46 Check that no such record exists\n});\n")),(0,s.kt)("h2",{id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much"},"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When deleting an existing order, Then it should NOT be retrievable', async () => {\n // Arrange\n const orderToDelete = {\n userId: 1,\n productId: 2,\n };\n const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data\n .id; // We will delete this soon\n const orderNotToBeDeleted = orderToDelete;\n const notDeletedOrder = (\n await axiosAPIClient.post('/order', orderNotToBeDeleted)\n ).data.id; // We will not delete this\n\n // Act\n await axiosAPIClient.delete(`/order/${deletedOrder}`);\n\n // Assert\n const { status: getDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${deletedOrder}`\n );\n const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${notDeletedOrder}`\n );\n expect(getNotDeletedOrderStatus).toBe(200);\n expect(getDeletedOrderStatus).toBe(404);\n});\n")),(0,s.kt)("h2",{id:"-the-slow-collaborator-test---when-the-other-http-service-times-out"},"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock")," or ",(0,s.kt)("a",{parentName:"p",href:"https://wiremock.org/"},"wiremock"),". These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available ",(0,s.kt)("strong",{parentName:"p"},"in production"),", what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use ",(0,s.kt)("a",{parentName:"p",href:"https://sinonjs.org/releases/latest/fake-timers/"},"fake timers")," and trick the system into believing as few seconds passed in a single tick. If you're using ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock"),", it offers an interesting feature to simulate timeouts ",(0,s.kt)("strong",{parentName:"p"},"quickly"),": the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// In this example, our code accepts new Orders and while processing them approaches the Users Microservice\ntest('When users service times out, then return 503 (option 1 with fake timers)', async () => {\n //Arrange\n const clock = sinon.useFakeTimers();\n config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls\n nock(`${config.userServiceURL}/user/`)\n .get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout \ud83d\udc46\n .reply(200);\n const loggerDouble = sinon.stub(logger, 'error');\n const orderToAdd = {\n userId: 1,\n productId: 2,\n mode: 'approved',\n };\n\n //Act\n // \ud83d\udc47try to add new order which should fail due to User service not available\n const response = await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n // \ud83d\udc47At least our code does its best given this situation\n expect(response.status).toBe(503);\n expect(loggerDouble.lastCall.firstArg).toMatchObject({\n name: 'user-service-not-available',\n stack: expect.any(String),\n message: expect.any(String),\n });\n});\n")),(0,s.kt)("h2",{id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation"},"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why"),(0,s.kt)("p",null,"When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. ",(0,s.kt)("a",{parentName:"p",href:"https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-using-purge-queue.html"},"SQS demand 60 seconds")," to purge queues), to name a few challenges that you won't find when dealing with real DB"),(0,s.kt)("p",null,"Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/m-radzikowski/aws-sdk-client-mock"},"this one for SQS")," and you can code one ",(0,s.kt)("strong",{parentName:"p"},"easily")," yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("ol",null,(0,s.kt)("li",{parentName:"ol"},"Create a fake message queue that does almost nothing but record calls, see full example here")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class FakeMessageQueueProvider extends EventEmitter {\n // Implement here\n\n publish(message) {}\n\n consume(queueName, callback) {}\n}\n")),(0,s.kt)("ol",{start:2},(0,s.kt)("li",{parentName:"ol"},"Make your message queue client accept real or fake provider")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n // Pass to it a fake or real message queue\n constructor(customMessageQueueProvider) {}\n\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // Simple implementation can be found here:\n // https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js\n}\n")),(0,s.kt)("ol",{start:3},(0,s.kt)("li",{parentName:"ol"},"Expose a convenient function that tells when certain calls where made")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // \ud83d\udc47\n waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise\n}\n")),(0,s.kt)("ol",{start:4},(0,s.kt)("li",{parentName:"ol"},"The test is now short, flat and expressive \ud83d\udc47")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');\nconst MessageQueueClient = require('./libs/message-queue-client');\nconst newOrderService = require('./domain/newOrderService');\n\ntest('When a poisoned message arrives, then it is being rejected back', async () => {\n // Arrange\n const messageWithInvalidSchema = { nonExistingProperty: 'invalid\u274c' };\n const messageQueueClient = new MessageQueueClient(\n new FakeMessageQueueProvider()\n );\n // Subscribe to new messages and passing the handler function\n messageQueueClient.consume('orders.new', newOrderService.addOrder);\n\n // Act\n await messageQueueClient.publish('orders.new', messageWithInvalidSchema);\n // Now all the layers of the app will get stretched \ud83d\udc46, including logic and message queue libraries\n\n // Assert\n await messageQueueClient.waitFor('reject', { howManyTimes: 1 });\n // \ud83d\udc46 This tells us that eventually our code asked the message queue client to reject this poisoned message\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/recipes/message-queue/fake-message-queue.test.js"},"is here")),(0,s.kt)("h2",{id:"-test-the-package-as-a-consumer"},"\ud83d\udce6 Test the package as a consumer"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts ",(0,s.kt)("em",{parentName:"p"},"that were built"),". See the mismatch here? ",(0,s.kt)("em",{parentName:"p"},"after")," running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,"Consider the following scenario, you're developing a library, and you wrote this code:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// index.js\nexport * from './calculate.js';\n\n// calculate.js \ud83d\udc48\nexport function calculate() {\n return 1;\n}\n")),(0,s.kt)("p",null,"Then some tests:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"import { calculate } from './index.js';\n\ntest('should return 1', () => {\n expect(calculate()).toBe(1);\n})\n\n\u2705 All tests pass \ud83c\udf8a\n")),(0,s.kt)("p",null,"Finally configure the package.json:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json5"},'{\n // ....\n "files": [\n "index.js"\n ]\n}\n')),(0,s.kt)("p",null,"See, 100% coverage, all tests pass locally and in the CI \u2705, it just won't work in production \ud83d\udc79. Why? because you forgot to include the ",(0,s.kt)("inlineCode",{parentName:"p"},"calculate.js")," in the package.json ",(0,s.kt)("inlineCode",{parentName:"p"},"files")," array \ud83d\udc46"),(0,s.kt)("p",null,"What can we do instead? we can test the library as ",(0,s.kt)("em",{parentName:"p"},"its end-users"),". How? publish the package to a local registry like ",(0,s.kt)("a",{parentName:"p",href:"https://verdaccio.org/"},"verdaccio"),", let the tests install and approach the ",(0,s.kt)("em",{parentName:"p"},"published")," code. Sounds troublesome? judge yourself \ud83d\udc47"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// global-setup.js\n\n// 1. Setup the in-memory NPM registry, one function that's it! \ud83d\udd25\nawait setupVerdaccio();\n\n// 2. Building our package \nawait exec('npm', ['run', 'build'], {\n cwd: packagePath,\n});\n\n// 3. Publish it to the in-memory registry\nawait exec('npm', ['publish', '--registry=http://localhost:4873'], {\n cwd: packagePath,\n});\n\n// 4. Installing it in the consumer directory\nawait exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {\n cwd: consumerPath,\n});\n\n// Test file in the consumerPath\n\n// 5. Test the package \ud83d\ude80\ntest(\"should succeed\", async () => {\n const { fn1 } = await import('my-package');\n\n expect(fn1()).toEqual(1);\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/rluvaton/e2e-verdaccio-example"},"is here")),(0,s.kt)("p",null,"What else this technique can be useful for?"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that"),(0,s.kt)("li",{parentName:"ul"},"You want to test ESM and CJS consumers"),(0,s.kt)("li",{parentName:"ul"},"If you have CLI application you can test it like your users"),(0,s.kt)("li",{parentName:"ul"},"Making sure all the voodoo magic in that babel file is working as expected")),(0,s.kt)("h2",{id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug"},"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -"),' Quite confidently I\'m sure that almost no team test their OpenAPI correctness. "It\'s just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.'),(0,s.kt)("p",null,"Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., ",(0,s.kt)("a",{parentName:"p",href:"https://pact.io"},"PACT"),"), there are also leaner approaches that gets you covered ",(0,s.kt)("em",{parentName:"p"},"easily and quickly")," (at the price of covering less risks)."),(0,s.kt)("p",null,"The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, an API throw a new error status")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"if (doesOrderCouponAlreadyExist) {\n throw new AppError('duplicated-coupon', { httpStatus: 409 });\n}\n")),(0,s.kt)("p",null,"The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json"},'"responses": {\n "200": {\n "description": "successful",\n }\n ,\n "400": {\n "description": "Invalid ID",\n "content": {}\n },// No 409 in this list\ud83d\ude32\ud83d\udc48\n}\n\n')),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const jestOpenAPI = require('jest-openapi');\njestOpenAPI('../openapi.json');\n\ntest('When an order with duplicated coupon is added , then 409 error should get returned', async () => {\n // Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n couponId: uuid(),\n };\n await axiosAPIClient.post('/order', orderToAdd);\n\n // Act\n // We're adding the same coupon twice \ud83d\udc47\n const receivedResponse = await axios.post('/order', orderToAdd);\n\n // Assert;\n expect(receivedResponse.status).toBe(409);\n expect(res).toSatisfyApiSpec();\n // This \ud83d\udc46 will throw if the API response, body or status, is different that was it stated in the OpenAPI\n});\n")),(0,s.kt)("p",null,"Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"beforeAll(() => {\n axios.interceptors.response.use((response) => {\n expect(response.toSatisfyApiSpec());\n // With this \ud83d\udc46, add nothing to the tests - each will fail if the response deviates from the docs\n });\n});\n")),(0,s.kt)("h2",{id:"even-more-ideas"},"Even more ideas"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Test readiness and health routes"),(0,s.kt)("li",{parentName:"ul"},"Test message queue connection failures"),(0,s.kt)("li",{parentName:"ul"},"Test JWT and JWKS failures"),(0,s.kt)("li",{parentName:"ul"},"Test security-related things like CSRF tokens"),(0,s.kt)("li",{parentName:"ul"},"Test your HTTP client retry mechanism (very easy with nock)"),(0,s.kt)("li",{parentName:"ul"},"Test that the DB migration succeed and the new code can work with old records format"),(0,s.kt)("li",{parentName:"ul"},"Test DB connection disconnects"),(0,s.kt)("li",{parentName:"ul"},"You may find many more examples at my fresh new testing course - ",(0,s.kt)("a",{parentName:"li",href:"https://testjavascript.com"},"testjavascript.com"))),(0,s.kt)("h2",{id:"its-not-just-ideas-it-a-whole-new-mindset"},"It's not just ideas, it a whole new mindset"),(0,s.kt)("p",null,"The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"My new online testing course -")," If you're intrigued with beyond the basics testing patterns, ]consider my online course which was just launched and is \ud83c\udf81 on sale for 30 days (July 2023)](",(0,s.kt)("a",{parentName:"p",href:"https://testjavascript.com"},"https://testjavascript.com"),")"))}u.isMDXComponent=!0},8933:(e,t,n)=>{n.d(t,{Z:()=>a});const a=n.p+"assets/images/the-hidden-corners-44855c2e5d9184502e1dc72b07d53cef.png"}}]); \ No newline at end of file diff --git a/assets/js/ea907698.850a16d9.js b/assets/js/ea907698.850a16d9.js deleted file mode 100644 index 1c638b1f..00000000 --- a/assets/js/ea907698.850a16d9.js +++ /dev/null @@ -1 +0,0 @@ -"use strict";(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[9362],{3905:(e,t,n)=>{n.d(t,{Zo:()=>u,kt:()=>p});var a=n(7294);function s(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function o(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function r(e){for(var t=1;t=0||(s[n]=e[n]);return s}(e,t);if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(a=0;a=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}var l=a.createContext({}),c=function(e){var t=a.useContext(l),n=t;return e&&(n="function"==typeof e?e(t):r(r({},t),e)),n},u=function(e){var t=c(e.components);return a.createElement(l.Provider,{value:t},e.children)},h={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},d=a.forwardRef((function(e,t){var n=e.components,s=e.mdxType,o=e.originalType,l=e.parentName,u=i(e,["components","mdxType","originalType","parentName"]),d=c(n),p=s,m=d["".concat(l,".").concat(p)]||d[p]||h[p]||o;return n?a.createElement(m,r(r({ref:t},u),{},{components:n})):a.createElement(m,r({ref:t},u))}));function p(e,t){var n=arguments,s=t&&t.mdxType;if("string"==typeof e||s){var o=n.length,r=new Array(o);r[0]=d;var i={};for(var l in t)hasOwnProperty.call(t,l)&&(i[l]=t[l]);i.originalType=e,i.mdxType="string"==typeof e?e:s,r[1]=i;for(var c=2;c{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>r,default:()=>h,frontMatter:()=>o,metadata:()=>i,toc:()=>c});var a=n(7462),s=(n(7294),n(3905));const o={slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},r=void 0,i={permalink:"/blog/testing-the-dark-scenarios-of-your-nodejs-application",editUrl:"https://github.com/practicajs/practica/tree/main/docs/blog/crucial-tests/index.md",source:"@site/blog/crucial-tests/index.md",title:"Testing the dark scenarios of your Node.js application",description:"Where the dead-bodies are covered",date:"2023-07-07T11:00:00.000Z",formattedDate:"July 7, 2023",tags:[{label:"node.js",permalink:"/blog/tags/node-js"},{label:"testing",permalink:"/blog/tags/testing"},{label:"component-test",permalink:"/blog/tags/component-test"},{label:"fastify",permalink:"/blog/tags/fastify"},{label:"unit-test",permalink:"/blog/tags/unit-test"},{label:"integration",permalink:"/blog/tags/integration"},{label:"nock",permalink:"/blog/tags/nock"}],readingTime:19.875,hasTruncateMarker:!1,authors:[{name:"Yoni Goldberg",title:"Practica.js core maintainer",url:"https://github.com/goldbergyoni",imageURL:"https://github.com/goldbergyoni.png",key:"goldbergyoni"},{name:"Raz Luvaton",title:"Practica.js core maintainer",url:"https://github.com/rluvaton",imageURL:"https://avatars.githubusercontent.com/u/16746759?v=4",key:"razluvaton"}],frontMatter:{slug:"testing-the-dark-scenarios-of-your-nodejs-application",date:"2023-07-07T11:00",hide_table_of_contents:!0,title:"Testing the dark scenarios of your Node.js application",authors:["goldbergyoni","razluvaton"],tags:["node.js","testing","component-test","fastify","unit-test","integration","nock"]},nextItem:{title:"Which Monorepo is right for a Node.js BACKEND\xa0now?",permalink:"/blog/monorepo-backend"}},l={authorsImageUrls:[void 0,void 0]},c=[{value:"Where the dead-bodies are covered",id:"where-the-dead-bodies-are-covered",level:2},{value:"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test",id:"\ufe0f-the-zombie-process-test",level:2},{value:"\ud83d\udc40 The observability test",id:"-the-observability-test",level:2},{value:"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code",id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code",level:2},{value:"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all",id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all",level:2},{value:"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much",id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much",level:2},{value:"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out",id:"-the-slow-collaborator-test---when-the-other-http-service-times-out",level:2},{value:"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation",id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation",level:2},{value:"\ud83d\udce6 Test the package as a consumer",id:"-test-the-package-as-a-consumer",level:2},{value:"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug",id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug",level:2},{value:"Even more ideas",id:"even-more-ideas",level:2},{value:"It's not just ideas, it a whole new mindset",id:"its-not-just-ideas-it-a-whole-new-mindset",level:2}],u={toc:c};function h(e){let{components:t,...o}=e;return(0,s.kt)("wrapper",(0,a.Z)({},u,o,{components:t,mdxType:"MDXLayout"}),(0,s.kt)("h2",{id:"where-the-dead-bodies-are-covered"},"Where the dead-bodies are covered"),(0,s.kt)("p",null,"This post is about tests that are easy to write, 5-8 lines typically, they cover dark and dangerous corners of our applications, but are often overlooked"),(0,s.kt)("p",null,"Some context first: How do we test a modern backend? With ",(0,s.kt)("a",{parentName:"p",href:"https://ritesh-kapoor.medium.com/testing-automation-what-are-pyramids-and-diamonds-67494fec7c55"},"the testing diamond"),", of course, by putting the focus on component/integration tests that cover all the layers, including a real DB. With this approach, our tests 99% resemble the production and the user flows, while the development experience is almost as good as with unit tests. Sweet. If this topic is of interest, we've also written ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices"},"a guide with 50 best practices for integration tests in Node.js")),(0,s.kt)("p",null,"But there is a pitfall: most developers write ",(0,s.kt)("em",{parentName:"p"},"only")," semi-happy test cases that are focused on the core user flows. Like invalid inputs, CRUD operations, various application states, etc. This is indeed the bread and butter, a great start, but a whole area is left uncovered. For example, typical tests don't simulate an unhandled promise rejection that leads to process crash, nor do they simulate the webserver bootstrap phase that might fail and leave the process idle, or HTTP calls to external services that often end with timeouts and retries. They typically not covering the health and readiness route, nor the integrity of the OpenAPI to the actual routes schema, to name just a few examples. There are many dead bodies covered beyond business logic, things that sometimes are even beyond bugs but rather are concerned with application downtime"),(0,s.kt)("p",null,(0,s.kt)("img",{alt:"The hidden corners",src:n(8933).Z,width:"900",height:"521"})),(0,s.kt)("p",null,"Here are a handful of examples that might open your mind to a whole new class of risks and tests"),(0,s.kt)("h2",{id:"\ufe0f-the-zombie-process-test"},"\ud83e\udddf\u200d\u2640\ufe0f The zombie process test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what? -")," In all of your tests, you assume that the app has already started successfully, lacking a test against the initialization flow. This is a pity because this phase hides some potential catastrophic failures: First, initialization failures are frequent - many bad things can happen here, like a DB connection failure or a new version that crashes during deployment. For this reason, runtime platforms (like Kubernetes and others) encourage components to signal when they are ready (see ",(0,s.kt)("a",{parentName:"p",href:"https://komodor.com/learn/kubernetes-readiness-probes-a-practical-guide/#:~:text=A%20readiness%20probe%20allows%20Kubernetes,on%20deletion%20of%20a%20pod."},"readiness probe"),"). Errors at this stage also have a dramatic effect over the app health - if the initialization fails and the process stays alive, it becomes a 'zombie process'. In this scenario, the runtime platform won't realize that something went bad, forward traffic to it and avoid creating alternative instances. Besides exiting gracefully, you may want to consider logging, firing a metric, and adjusting your /readiness route. Does it work? only test can tell!"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, api.js:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// A common express server initialization\nconst startWebServer = () => {\n return new Promise((resolve, reject) => {\n try {\n // A typical Express setup\n expressApp = express();\n defineRoutes(expressApp); // a function that defines all routes\n expressApp.listen(process.env.WEB_SERVER_PORT);\n } catch (error) {\n //log here, fire a metric, maybe even retry and finally:\n process.exit();\n }\n });\n};\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test:")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const api = require('./entry-points/api'); // our api starter that exposes 'startWebServer' function\nconst sinon = require('sinon'); // a mocking library\n\ntest('When an error happens during the startup phase, then the process exits', async () => {\n // Arrange\n const processExitListener = sinon.stub(process, 'exit');\n // \ud83d\udc47 Choose a function that is part of the initialization phase and make it fail\n sinon\n .stub(routes, 'defineRoutes')\n .throws(new Error('Cant initialize connection'));\n\n // Act\n await api.startWebServer();\n\n // Assert\n expect(processExitListener.called).toBe(true);\n});\n")),(0,s.kt)("h2",{id:"-the-observability-test"},"\ud83d\udc40 The observability test"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," For many, testing error means checking the exception type or the API response. This leaves one of the most essential parts uncovered - making the error ",(0,s.kt)("strong",{parentName:"p"},"correctly observable"),". In plain words, ensuring that it's being logged correctly and exposed to the monitoring system. It might sound like an internal thing, implementation testing, but actually, it goes directly to a user. Yes, not the end-user, but rather another important one - the ops user who is on-call. What are the expectations of this user? At the very basic level, when a production issue arises, she must see detailed log entries, ",(0,s.kt)("em",{parentName:"p"},"including stack trace"),", cause and other properties. This info can save the day when dealing with production incidents. On to of this, in many systems, monitoring is managed separately to conclude about the overall system state using cumulative heuristics (e.g., an increase in the number of errors over the last 3 hours). To support this monitoring needs, the code also must fire error metrics. Even tests that do try to cover these needs take a naive approach by checking that the logger function was called - but hey, does it include the right data? Some write better tests that check the error type that was passed to the logger, good enough? No! The ops user doesn't care about the JavaScript class names but the JSON data that is sent out. The following test focuses on the specific properties that are being made observable:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When exception is throw during request, Then logger reports the mandatory fields', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n status: 'approved',\n };\n const metricsExporterDouble = sinon.stub(metricsExporter, 'fireMetric');\n sinon\n .stub(OrderRepository.prototype, 'addOrder')\n .rejects(new AppError('saving-failed', 'Order could not be saved', 500));\n const loggerDouble = sinon.stub(logger, 'error');\n\n //Act\n await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n expect(loggerDouble).toHaveBeenCalledWith({\n name: 'saving-failed',\n status: 500,\n stack: expect.any(String),\n message: expect.any(String),\n });\n expect(\n metricsExporterDouble).toHaveBeenCalledWith('error', {\n errorName: 'example-error',\n })\n});\n")),(0,s.kt)("h2",{id:"-the-unexpected-visitor-test---when-an-uncaught-exception-meets-our-code"},"\ud83d\udc7d The 'unexpected visitor' test - when an uncaught exception meets our code"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," A typical error flow test falsely assumes two conditions: A valid error object was thrown, and it was caught. Neither is guaranteed, let's focus on the 2nd assumption: it's common for certain errors to left uncaught. The error might get thrown before your framework error handler is ready, some npm libraries can throw surprisingly from different stacks using timer functions, or you just forget to set someEventEmitter.on('error', ...). To name a few examples. These errors will find their way to the global process.on('uncaughtException') handler, ",(0,s.kt)("strong",{parentName:"p"},"hopefully if your code subscribed"),". How do you simulate this scenario in a test? naively you may locate a code area that is not wrapped with try-catch and stub it to throw during the test. But here's a catch22: if you are familiar with such area - you are likely to fix it and ensure its errors are caught. What do we do then? we can bring to our benefit the fact the JavaScript is 'borderless', if some object can emit an event, we as its subscribers can make it emit this event ourselves, here's an example:"),(0,s.kt)("p",null,"researches says that, rejection"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When an unhandled exception is thrown, then process stays alive and the error is logged', async () => {\n //Arrange\n const loggerDouble = sinon.stub(logger, 'error');\n const processExitListener = sinon.stub(process, 'exit');\n const errorToThrow = new Error('An error that wont be caught \ud83d\ude33');\n\n //Act\n process.emit('uncaughtException', errorToThrow); //\ud83d\udc48 Where the magic is\n\n // Assert\n expect(processExitListener.called).toBe(false);\n expect(loggerDouble).toHaveBeenCalledWith(errorToThrow);\n});\n")),(0,s.kt)("h2",{id:"-the-hidden-effect-test---when-the-code-should-not-mutate-at-all"},"\ud83d\udd75\ud83c\udffc The 'hidden effect' test - when the code should not mutate at all"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," In common scenarios, the code under test should stop early like when the incoming payload is invalid or a user doesn't have sufficient credits to perform an operation. In these cases, no DB records should be mutated. Most tests out there in the wild settle with testing the HTTP response only - got back HTTP 400? great, the validation/authorization probably work. Or does it? The test trusts the code too much, a valid response doesn't guarantee that the code behind behaved as design. Maybe a new record was added although the user has no permissions? Clearly you need to test this, but how would you test that a record was NOT added? There are two options here: If the DB is purged before/after every test, than just try to perform an invalid operation and check that the DB is empty afterward. If you're not cleaning the DB often (like me, but that's another discussion), the payload must contain some unique and queryable value that you can query later and hope to get no records. This is how it looks like:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"it('When adding an invalid order, then it returns 400 and NOT retrievable', async () => {\n //Arrange\n const orderToAdd = {\n userId: 1,\n mode: 'draft',\n externalIdentifier: uuid(), //no existing record has this value\n };\n\n //Act\n const { status: addingHTTPStatus } = await axiosAPIClient.post(\n '/order',\n orderToAdd\n );\n\n //Assert\n const { status: fetchingHTTPStatus } = await axiosAPIClient.get(\n `/order/externalIdentifier/${orderToAdd.externalIdentifier}`\n ); // Trying to get the order that should have failed\n expect({ addingHTTPStatus, fetchingHTTPStatus }).toMatchObject({\n addingHTTPStatus: 400,\n fetchingHTTPStatus: 404,\n });\n // \ud83d\udc46 Check that no such record exists\n});\n")),(0,s.kt)("h2",{id:"-the-overdoing-test---when-the-code-should-mutate-but-its-doing-too-much"},"\ud83e\udde8 The 'overdoing' test - when the code should mutate but it's doing too much"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," This is how a typical data-oriented test looks like: first you add some records, then approach the code under test, and finally assert what happens to these specific records. So far, so good. There is one caveat here though: since the test narrows it focus to specific records, it ignores whether other record were unnecessarily affected. This can be really bad, here's a short real-life story that happened to my customer: Some data access code changed and incorporated a bug that updates ALL the system users instead of just one. All test pass since they focused on a specific record which positively updated, they just ignored the others. How would you test and prevent? here is a nice trick that I was taught by my friend Gil Tayar: in the first phase of the test, besides the main records, add one or more 'control' records that should not get mutated during the test. Then, run the code under test, and besides the main assertion, check also that the control records were not affected:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"test('When deleting an existing order, Then it should NOT be retrievable', async () => {\n // Arrange\n const orderToDelete = {\n userId: 1,\n productId: 2,\n };\n const deletedOrder = (await axiosAPIClient.post('/order', orderToDelete)).data\n .id; // We will delete this soon\n const orderNotToBeDeleted = orderToDelete;\n const notDeletedOrder = (\n await axiosAPIClient.post('/order', orderNotToBeDeleted)\n ).data.id; // We will not delete this\n\n // Act\n await axiosAPIClient.delete(`/order/${deletedOrder}`);\n\n // Assert\n const { status: getDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${deletedOrder}`\n );\n const { status: getNotDeletedOrderStatus } = await axiosAPIClient.get(\n `/order/${notDeletedOrder}`\n );\n expect(getNotDeletedOrderStatus).toBe(200);\n expect(getDeletedOrderStatus).toBe(404);\n});\n")),(0,s.kt)("h2",{id:"-the-slow-collaborator-test---when-the-other-http-service-times-out"},"\ud83d\udd70 The 'slow collaborator' test - when the other HTTP service times out"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When your code approaches other services/microservices via HTTP, savvy testers minimize end-to-end tests because these tests lean toward happy paths (it's harder to simulate scenarios). This mandates using some mocking tool to act like the remote service, for example, using tools like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock")," or ",(0,s.kt)("a",{parentName:"p",href:"https://wiremock.org/"},"wiremock"),". These tools are great, only some are using them naively and check mainly that calls outside were indeed made. What if the other service is not available ",(0,s.kt)("strong",{parentName:"p"},"in production"),", what if it is slower and times out occasionally (one of the biggest risks of Microservices)? While you can't wholly save this transaction, your code should do the best given the situation and retry, or at least log and return the right status to the caller. All the network mocking tools allow simulating delays, timeouts and other 'chaotic' scenarios. Question left is how to simulate slow response without having slow tests? You may use ",(0,s.kt)("a",{parentName:"p",href:"https://sinonjs.org/releases/latest/fake-timers/"},"fake timers")," and trick the system into believing as few seconds passed in a single tick. If you're using ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/nock/nock"},"nock"),", it offers an interesting feature to simulate timeouts ",(0,s.kt)("strong",{parentName:"p"},"quickly"),": the .delay function simulates slow responses, then nock will realize immediately if the delay is higher than the HTTP client timeout and throw a timeout event immediately without waiting"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"// In this example, our code accepts new Orders and while processing them approaches the Users Microservice\ntest('When users service times out, then return 503 (option 1 with fake timers)', async () => {\n //Arrange\n const clock = sinon.useFakeTimers();\n config.HTTPCallTimeout = 1000; // Set a timeout for outgoing HTTP calls\n nock(`${config.userServiceURL}/user/`)\n .get('/1', () => clock.tick(2000)) // Reply delay is bigger than configured timeout \ud83d\udc46\n .reply(200);\n const loggerDouble = sinon.stub(logger, 'error');\n const orderToAdd = {\n userId: 1,\n productId: 2,\n mode: 'approved',\n };\n\n //Act\n // \ud83d\udc47try to add new order which should fail due to User service not available\n const response = await axiosAPIClient.post('/order', orderToAdd);\n\n //Assert\n // \ud83d\udc47At least our code does its best given this situation\n expect(response.status).toBe(503);\n expect(loggerDouble.lastCall.firstArg).toMatchObject({\n name: 'user-service-not-available',\n stack: expect.any(String),\n message: expect.any(String),\n });\n});\n")),(0,s.kt)("h2",{id:"-the-poisoned-message-test---when-the-message-consumer-gets-an-invalid-payload-that-might-put-it-in-stagnation"},"\ud83d\udc8a The 'poisoned message' test - when the message consumer gets an invalid payload that might put it in stagnation"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -")," When testing flows that start or end in a queue, I bet you're going to bypass the message queue layer, where the code and libraries consume a queue, and you approach the logic layer directly. Yes, it makes things easier but leaves a class of uncovered risks. For example, what if the logic part throws an error or the message schema is invalid but the message queue consumer fails to translate this exception into a proper message queue action? For example, the consumer code might fail to reject the message or increment the number of attempts (depends on the type of queue that you're using). When this happens, the message will enter a loop where it always served again and again. Since this will apply to many messages, things can get really bad as the queue gets highly saturated. For this reason this syndrome was called the 'poisoned message'. To mitigate this risk, the tests' scope must include all the layers like how you probably do when testing against APIs. Unfortunately, this is not as easy as testing with DB because message queues are flaky, here is why"),(0,s.kt)("p",null,"When testing with real queues things get curios and curiouser: tests from different process will steal messages from each other, purging queues is harder that you might think (e.g. ",(0,s.kt)("a",{parentName:"p",href:"https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-using-purge-queue.html"},"SQS demand 60 seconds")," to purge queues), to name a few challenges that you won't find when dealing with real DB"),(0,s.kt)("p",null,"Here is a strategy that works for many teams and holds a small compromise - use a fake in-memory message queue. By 'fake' I mean something simplistic that acts like a stub/spy and do nothing but telling when certain calls are made (e.g., consume, delete, publish). You might find reputable fakes/stubs for your own message queue like ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/m-radzikowski/aws-sdk-client-mock"},"this one for SQS")," and you can code one ",(0,s.kt)("strong",{parentName:"p"},"easily")," yourself. No worries, I'm not a favour of maintaining myself testing infrastructure, this proposed component is extremely simply and unlikely to surpass 50 lines of code (see example below). On top of this, whether using a real or fake queue, one more thing is needed: create a convenient interface that tells to the test when certain things happened like when a message was acknowledged/deleted or a new message was published. Without this, the test never knows when certain events happened and lean toward quirky techniques like polling. Having this setup, the test will be short, flat and you can easily simulate common message queue scenarios like out of order messages, batch reject, duplicated messages and in our example - the poisoned message scenario (using RabbitMQ):"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("ol",null,(0,s.kt)("li",{parentName:"ol"},"Create a fake message queue that does almost nothing but record calls, see full example here")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class FakeMessageQueueProvider extends EventEmitter {\n // Implement here\n\n publish(message) {}\n\n consume(queueName, callback) {}\n}\n")),(0,s.kt)("ol",{start:2},(0,s.kt)("li",{parentName:"ol"},"Make your message queue client accept real or fake provider")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n // Pass to it a fake or real message queue\n constructor(customMessageQueueProvider) {}\n\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // Simple implementation can be found here:\n // https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/example-application/libraries/fake-message-queue-provider.js\n}\n")),(0,s.kt)("ol",{start:3},(0,s.kt)("li",{parentName:"ol"},"Expose a convenient function that tells when certain calls where made")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"class MessageQueueClient extends EventEmitter {\n publish(message) {}\n\n consume(queueName, callback) {}\n\n // \ud83d\udc47\n waitForEvent(eventName: 'publish' | 'consume' | 'acknowledge' | 'reject', howManyTimes: number) : Promise\n}\n")),(0,s.kt)("ol",{start:4},(0,s.kt)("li",{parentName:"ol"},"The test is now short, flat and expressive \ud83d\udc47")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const FakeMessageQueueProvider = require('./libs/fake-message-queue-provider');\nconst MessageQueueClient = require('./libs/message-queue-client');\nconst newOrderService = require('./domain/newOrderService');\n\ntest('When a poisoned message arrives, then it is being rejected back', async () => {\n // Arrange\n const messageWithInvalidSchema = { nonExistingProperty: 'invalid\u274c' };\n const messageQueueClient = new MessageQueueClient(\n new FakeMessageQueueProvider()\n );\n // Subscribe to new messages and passing the handler function\n messageQueueClient.consume('orders.new', newOrderService.addOrder);\n\n // Act\n await messageQueueClient.publish('orders.new', messageWithInvalidSchema);\n // Now all the layers of the app will get stretched \ud83d\udc46, including logic and message queue libraries\n\n // Assert\n await messageQueueClient.waitFor('reject', { howManyTimes: 1 });\n // \ud83d\udc46 This tells us that eventually our code asked the message queue client to reject this poisoned message\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/testjavascript/nodejs-integration-tests-best-practices/blob/master/recipes/message-queue/fake-message-queue.test.js"},"is here")),(0,s.kt)("h2",{id:"-test-the-package-as-a-consumer"},"\ud83d\udce6 Test the package as a consumer"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & why -")," When publishing a library to npm, easily all your tests might pass BUT... the same functionality will fail over the end-user's computer. How come? tests are executed against the local developer files, but the end-user is only exposed to artifacts ",(0,s.kt)("em",{parentName:"p"},"that were built"),". See the mismatch here? ",(0,s.kt)("em",{parentName:"p"},"after")," running the tests, the package files are transpiled (I'm looking at you babel users), zipped and packed. If a single file is excluded due to .npmignore or a polyfill is not added correctly, the published code will lack mandatory files"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,"Consider the following scenario, you're developing a library, and you wrote this code:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// index.js\nexport * from './calculate.js';\n\n// calculate.js \ud83d\udc48\nexport function calculate() {\n return 1;\n}\n")),(0,s.kt)("p",null,"Then some tests:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"import { calculate } from './index.js';\n\ntest('should return 1', () => {\n expect(calculate()).toBe(1);\n})\n\n\u2705 All tests pass \ud83c\udf8a\n")),(0,s.kt)("p",null,"Finally configure the package.json:"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json5"},'{\n // ....\n "files": [\n "index.js"\n ]\n}\n')),(0,s.kt)("p",null,"See, 100% coverage, all tests pass locally and in the CI \u2705, it just won't work in production \ud83d\udc79. Why? because you forgot to include the ",(0,s.kt)("inlineCode",{parentName:"p"},"calculate.js")," in the package.json ",(0,s.kt)("inlineCode",{parentName:"p"},"files")," array \ud83d\udc46"),(0,s.kt)("p",null,"What can we do instead? we can test the library as ",(0,s.kt)("em",{parentName:"p"},"its end-users"),". How? publish the package to a local registry like ",(0,s.kt)("a",{parentName:"p",href:"https://verdaccio.org/"},"verdaccio"),", let the tests install and approach the ",(0,s.kt)("em",{parentName:"p"},"published")," code. Sounds troublesome? judge yourself \ud83d\udc47"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-js"},"// global-setup.js\n\n// 1. Setup the in-memory NPM registry, one function that's it! \ud83d\udd25\nawait setupVerdaccio();\n\n// 2. Building our package \nawait exec('npm', ['run', 'build'], {\n cwd: packagePath,\n});\n\n// 3. Publish it to the in-memory registry\nawait exec('npm', ['publish', '--registry=http://localhost:4873'], {\n cwd: packagePath,\n});\n\n// 4. Installing it in the consumer directory\nawait exec('npm', ['install', 'my-package', '--registry=http://localhost:4873'], {\n cwd: consumerPath,\n});\n\n// Test file in the consumerPath\n\n// 5. Test the package \ud83d\ude80\ntest(\"should succeed\", async () => {\n const { fn1 } = await import('my-package');\n\n expect(fn1()).toEqual(1);\n});\n")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcddFull code example -")," ",(0,s.kt)("a",{parentName:"p",href:"https://github.com/rluvaton/e2e-verdaccio-example"},"is here")),(0,s.kt)("p",null,"What else this technique can be useful for?"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Testing different version of peer dependency you support - let's say your package support react 16 to 18, you can now test that"),(0,s.kt)("li",{parentName:"ul"},"You want to test ESM and CJS consumers"),(0,s.kt)("li",{parentName:"ul"},"If you have CLI application you can test it like your users"),(0,s.kt)("li",{parentName:"ul"},"Making sure all the voodoo magic in that babel file is working as expected")),(0,s.kt)("h2",{id:"-the-broken-contract-test---when-the-code-is-great-but-its-corresponding-openapi-docs-leads-to-a-production-bug"},"\ud83d\uddde The 'broken contract' test - when the code is great but its corresponding OpenAPI docs leads to a production bug"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udc49What & so what -"),' Quite confidently I\'m sure that almost no team test their OpenAPI correctness. "It\'s just documentation", "we generate it automatically based on code" are typical belief found for this reason. Let me show you how this auto generated documentation can be wrong and lead not only to frustration but also to a bug. In production.'),(0,s.kt)("p",null,"Consider the following scenario, you're requested to return HTTP error status code if an order is duplicated but forget to update the OpenAPI specification with this new HTTP status response. While some framework can update the docs with new fields, none can realize which errors your code throws, this labour is always manual. On the other side of the line, the API client is doing everything just right, going by the spec that you published, adding orders with some duplication because the docs don't forbid doing so. Then, BOOM, production bug -> the client crashes and shows an ugly unknown error message to the user. This type of failure is called the 'contract' problem when two parties interact, each has a code that works perfect, they just operate under different spec and assumptions. While there are fancy sophisticated and exhaustive solution to this challenge (e.g., ",(0,s.kt)("a",{parentName:"p",href:"https://pact.io"},"PACT"),"), there are also leaner approaches that gets you covered ",(0,s.kt)("em",{parentName:"p"},"easily and quickly")," (at the price of covering less risks)."),(0,s.kt)("p",null,"The following sweet technique is based on libraries (jest, mocha) that listen to all network responses, compare the payload against the OpenAPI document, and if any deviation is found - make the test fail with a descriptive error. With this new weapon in your toolbox and almost zero effort, another risk is ticked. It's a pity that these libs can't assert also against the incoming requests to tell you that your tests use the API wrong. One small caveat and an elegant solution: These libraries dictate putting an assertion statement in every test - expect(response).toSatisfyApiSpec(), a bit tedious and relies on human discipline. You can do better if your HTTP client supports plugin/hook/interceptor by putting this assertion in a single place that will apply in all the tests:"),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"\ud83d\udcdd Code")),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"Code under test, an API throw a new error status")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"if (doesOrderCouponAlreadyExist) {\n throw new AppError('duplicated-coupon', { httpStatus: 409 });\n}\n")),(0,s.kt)("p",null,"The OpenAPI doesn't document HTTP status '409', no framework knows to update the OpenAPI doc based on thrown exceptions"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-json"},'"responses": {\n "200": {\n "description": "successful",\n }\n ,\n "400": {\n "description": "Invalid ID",\n "content": {}\n },// No 409 in this list\ud83d\ude32\ud83d\udc48\n}\n\n')),(0,s.kt)("p",null,(0,s.kt)("strong",{parentName:"p"},"The test code")),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"const jestOpenAPI = require('jest-openapi');\njestOpenAPI('../openapi.json');\n\ntest('When an order with duplicated coupon is added , then 409 error should get returned', async () => {\n // Arrange\n const orderToAdd = {\n userId: 1,\n productId: 2,\n couponId: uuid(),\n };\n await axiosAPIClient.post('/order', orderToAdd);\n\n // Act\n // We're adding the same coupon twice \ud83d\udc47\n const receivedResponse = await axios.post('/order', orderToAdd);\n\n // Assert;\n expect(receivedResponse.status).toBe(409);\n expect(res).toSatisfyApiSpec();\n // This \ud83d\udc46 will throw if the API response, body or status, is different that was it stated in the OpenAPI\n});\n")),(0,s.kt)("p",null,"Trick: If your HTTP client supports any kind of plugin/hook/interceptor, put the following code in 'beforeAll'. This covers all the tests against OpenAPI mismatches"),(0,s.kt)("pre",null,(0,s.kt)("code",{parentName:"pre",className:"language-javascript"},"beforeAll(() => {\n axios.interceptors.response.use((response) => {\n expect(response.toSatisfyApiSpec());\n // With this \ud83d\udc46, add nothing to the tests - each will fail if the response deviates from the docs\n });\n});\n")),(0,s.kt)("h2",{id:"even-more-ideas"},"Even more ideas"),(0,s.kt)("ul",null,(0,s.kt)("li",{parentName:"ul"},"Test readiness and health routes"),(0,s.kt)("li",{parentName:"ul"},"Test message queue connection failures"),(0,s.kt)("li",{parentName:"ul"},"Test JWT and JWKS failures"),(0,s.kt)("li",{parentName:"ul"},"Test security-related things like CSRF tokens"),(0,s.kt)("li",{parentName:"ul"},"Test your HTTP client retry mechanism (very easy with nock)"),(0,s.kt)("li",{parentName:"ul"},"Test that the DB migration succeed and the new code can work with old records format"),(0,s.kt)("li",{parentName:"ul"},"Test DB connection disconnects")),(0,s.kt)("h2",{id:"its-not-just-ideas-it-a-whole-new-mindset"},"It's not just ideas, it a whole new mindset"),(0,s.kt)("p",null,"The examples above were not meant only to be a checklist of 'don't forget' test cases, but rather a fresh mindset on what tests could cover for you. Modern tests are not just about functions, or user flows, but any risk that might visit your production. This is doable only with component/integration tests but never with unit or end-to-end tests. Why? Because unlike unit you need all the parts to play together (e.g., the DB migration file, with the DAL layer and the error handler all together). Unlike E2E, you have the power to simulate in-process scenarios that demand some tweaking and mocking. Component tests allow you to include many production moving parts early on your machine. I like calling this 'production-oriented development'"))}h.isMDXComponent=!0},8933:(e,t,n)=>{n.d(t,{Z:()=>a});const a=n.p+"assets/images/the-hidden-corners-44855c2e5d9184502e1dc72b07d53cef.png"}}]); \ No newline at end of file diff --git a/assets/js/main.342a187c.js b/assets/js/main.f6bb4e9b.js similarity index 99% rename from assets/js/main.342a187c.js rename to assets/js/main.f6bb4e9b.js index 3bf52800..81d75b6e 100644 --- a/assets/js/main.342a187c.js +++ b/assets/js/main.f6bb4e9b.js @@ -1,2 +1,2 @@ -/*! For license information please see main.342a187c.js.LICENSE.txt */ -(self.webpackChunkpractica_docs=self.webpackChunkpractica_docs||[]).push([[179],{723:(e,t,n)=>{"use strict";n.d(t,{Z:()=>p});var r=n(7294),a=n(7462),o=n(8356),i=n.n(o),l=n(6887);const s={"01a85c17":[()=>Promise.all([n.e(532),n.e(4013)]).then(n.bind(n,1223)),"@theme/BlogTagsListPage",1223],"04975d12":[()=>n.e(7606).then(n.t.bind(n,2044,19)),"~blog/default/blog-tags-nock-b7a-list.json",2044],"0a44bc10":[()=>n.e(6764).then(n.t.bind(n,706,19)),"~blog/default/blog-tags-practica-aca-list.json",706],"0f5bddc1":[()=>n.e(1981).then(n.t.bind(n,2245,19)),"~blog/default/blog-tags-supertest-515.json",2245],"14f3c1c8":[()=>n.e(2).then(n.t.bind(n,9748,19)),"~blog/default/blog-tags-prisma-14c-list.json",9748],"15b89b76":[()=>n.e(8392).then(n.t.bind(n,9610,19)),"~blog/default/blog-tags-testing-92e.json",9610],17896441:[()=>Promise.all([n.e(532),n.e(7036),n.e(7918)]).then(n.bind(n,9055)),"@theme/DocItem",9055],"17da2d17":[()=>n.e(4077).then(n.bind(n,9387)),"@site/blog/practica-is-alive/index.md",9387],"1a7530a6":[()=>n.e(3491).then(n.bind(n,3808)),"@site/docs/features-reference.md",3808],"1be78505":[()=>Promise.all([n.e(532),n.e(9514)]).then(n.bind(n,9963)),"@theme/DocPage",9963],"1fe9a2e9":[()=>n.e(3409).then(n.bind(n,3748)),"@site/blog/pattern-to-reconsider/index.md?truncated=true",3748],"211a5e1a":[()=>n.e(8870).then(n.bind(n,8139)),"@site/docs/contribution/contribution-long-guide.md",8139],"27c1859b":[()=>n.e(2192).then(n.t.bind(n,2706,19)),"~blog/default/blog-tags-practica-aca.json",2706],"2b2237c5":[()=>n.e(7038).then(n.t.bind(n,3769,19)),"/home/runner/work/practica/practica/docs/.docusaurus/docusaurus-plugin-content-docs/default/plugin-route-context-module-100.json",3769],"2e8e3662":[()=>n.e(907).then(n.t.bind(n,3886,19)),"~blog/default/blog-tags-nock-b7a.json",3886],"2fdff385":[()=>n.e(8033).then(n.t.bind(n,4284,19)),"~blog/default/blog-tags-decisions-c33.json",4284],"379b65ab":[()=>n.e(2182).then(n.bind(n,7281)),"@site/blog/pattern-to-reconsider/index.md",7281],"39bbf0fd":[()=>n.e(4636).then(n.bind(n,9241)),"@site/blog/v0.6-is-alive/index.md?truncated=true",9241],"3a5322a7":[()=>n.e(9389).then(n.t.bind(n,3305,19)),"~blog/default/blog-tags-node-js-2dd-list.json",3305],"3aded9a5":[()=>n.e(1019).then(n.bind(n,7337)),"@site/blog/is-prisma-better/index.md?truncated=true",7337],"3d9c95a4":[()=>n.e(2125).then(n.bind(n,3191)),"@site/docs/home.md",3191],"4067f4ab":[()=>n.e(3668).then(n.t.bind(n,8326,19)),"~blog/default/blog-tags-unit-test-044.json",8326],"4bb443f0":[()=>n.e(4078).then(n.t.bind(n,9731,19)),"~blog/default/blog-tags-testing-92e-list.json",9731],"4e20cbbc":[()=>n.e(9313).then(n.t.bind(n,8115,19)),"~blog/default/blog-tags-integration-a8f.json",8115],"51736f2d":[()=>n.e(6705).then(n.bind(n,5836)),"@site/blog/is-prisma-better/index.md",5836],"5484f123":[()=>n.e(2029).then(n.bind(n,2502)),"@site/docs/the-basics/getting-started-quickly.md",2502],"5e729dc7":[()=>n.e(1353).then(n.t.bind(n,6603,19)),"~blog/default/blog-tags-integration-a8f-list.json",6603],"621e7957":[()=>n.e(943).then(n.bind(n,7278)),"@site/blog/practica-is-alive/index.md?truncated=true",7278],"6875c492":[()=>Promise.all([n.e(532),n.e(7036),n.e(6048),n.e(8610)]).then(n.bind(n,1714)),"@theme/BlogTagsPostsPage",1714],"69404bc7":[()=>n.e(9713).then(n.bind(n,4402)),"@site/docs/the-basics/coding-with-practica.md",4402],"710c3838":[()=>n.e(5819).then(n.bind(n,7955)),"@site/docs/contribution/contribution-short-guide.md",7955],"7302b0ae":[()=>n.e(5754).then(n.t.bind(n,6967,19)),"~blog/default/blog-tags-component-test-e8f-list.json",6967],"785487f7":[()=>n.e(9690).then(n.t.bind(n,8114,19)),"~blog/default/blog-tags-passport-f6e.json",8114],"79d3ae8c":[()=>n.e(3575).then(n.t.bind(n,8065,19)),"~blog/default/blog-tags-fastify-887-list.json",8065],"7abf8f9a":[()=>n.e(7018).then(n.bind(n,2697)),"@site/docs/decisions/openapi.md",2697],"7fe44762":[()=>n.e(8005).then(n.t.bind(n,5187,19)),"~blog/default/blog-tags-passport-f6e-list.json",5187],"814f3328":[()=>n.e(2535).then(n.t.bind(n,5641,19)),"~blog/default/blog-post-list-prop-default.json",5641],"8845108d":[()=>n.e(589).then(n.t.bind(n,9916,19)),"~blog/default/blog-tags-nestjs-fe7.json",9916],"89aeea8d":[()=>n.e(9779).then(n.t.bind(n,5644,19)),"~blog/default/blog-tags-fastify-887.json",5644],"8a07c89a":[()=>n.e(5586).then(n.t.bind(n,3697,19)),"~blog/default/blog-tags-decisions-c33-list.json",3697],"8bbcec4e":[()=>n.e(9460).then(n.bind(n,6456)),"@site/docs/decisions/configuration-library.md",6456],"91a0ce14":[()=>n.e(9476).then(n.bind(n,7865)),"@site/docs/decisions/docker-base-image.md",7865],93571253:[()=>n.e(1265).then(n.bind(n,6596)),"@site/blog/crucial-tests/index.md?truncated=true",6596],"935f2afb":[()=>n.e(53).then(n.t.bind(n,1109,19)),"~docs/default/version-current-metadata-prop-751.json",1109],"98caa824":[()=>n.e(4569).then(n.t.bind(n,4489,19)),"~blog/default/blog-tags-prisma-14c.json",4489],"9e4087bc":[()=>n.e(3608).then(n.bind(n,3169)),"@theme/BlogArchivePage",3169],a581386a:[()=>n.e(2872).then(n.bind(n,113)),"@site/docs/decisions/index.md",113],a6aa9e1f:[()=>Promise.all([n.e(532),n.e(7036),n.e(6048),n.e(3089)]).then(n.bind(n,46)),"@theme/BlogListPage",46],a7023ddc:[()=>n.e(1713).then(n.t.bind(n,3457,19)),"~blog/default/blog-tags-tags-4c2.json",3457],ace589d9:[()=>n.e(8716).then(n.bind(n,7159)),"@site/docs/questions-and-answers.md",7159],af8f0ebc:[()=>n.e(1337).then(n.bind(n,1079)),"@site/docs/decisions/monorepo.md",1079],b143667f:[()=>n.e(3478).then(n.bind(n,9552)),"@site/docs/the-basics/what-is-practica.md",9552],b2b675dd:[()=>n.e(533).then(n.t.bind(n,8017,19)),"~blog/default/blog-c06.json",8017],b2f554cd:[()=>n.e(1477).then(n.t.bind(n,10,19)),"~blog/default/blog-archive-80c.json",10],b972506a:[()=>n.e(1187).then(n.t.bind(n,6037,19)),"~blog/default/blog-tags-monorepo-88d.json",6037],bc5abee9:[()=>n.e(5880).then(n.t.bind(n,9122,19)),"~blog/default/blog-tags-express-964.json",9122],c0b8e344:[()=>n.e(7406).then(n.t.bind(n,3983,19)),"~blog/default/blog-tags-supertest-515-list.json",3983],c206e063:[()=>n.e(5705).then(n.t.bind(n,4469,19)),"/home/runner/work/practica/practica/docs/.docusaurus/docusaurus-plugin-content-blog/default/plugin-route-context-module-100.json",4469],cc670dbc:[()=>n.e(2093).then(n.t.bind(n,7453,19)),"~blog/default/blog-tags-node-js-2dd.json",7453],ccc49370:[()=>Promise.all([n.e(532),n.e(7036),n.e(6048),n.e(6103)]).then(n.bind(n,5203)),"@theme/BlogPostPage",5203],cd35e36e:[()=>n.e(1001).then(n.t.bind(n,9565,19)),"~blog/default/blog-tags-dotenv-3e7-list.json",9565],d0636688:[()=>n.e(9149).then(n.t.bind(n,3373,19)),"~blog/default/blog-tags-component-test-e8f.json",3373],d2a399e8:[()=>n.e(366).then(n.bind(n,4355)),"@site/blog/which-monorepo/index.md?truncated=true",4355],dac877fa:[()=>n.e(1867).then(n.bind(n,6012)),"@site/blog/v0.6-is-alive/index.md",6012],e25a597f:[()=>n.e(9768).then(n.bind(n,5849)),"@site/blog/which-monorepo/index.md",5849],e28473c3:[()=>n.e(4252).then(n.t.bind(n,1461,19)),"~blog/default/blog-tags-dotenv-3e7.json",1461],e4eaf29e:[()=>n.e(7043).then(n.bind(n,6745)),"@site/docs/contribution/release-checklist.md",6745],ea907698:[()=>n.e(9362).then(n.bind(n,3143)),"@site/blog/crucial-tests/index.md",3143],ed26bce9:[()=>n.e(6835).then(n.t.bind(n,6757,19)),"~blog/default/blog-tags-monorepo-88d-list.json",6757],f127536a:[()=>n.e(1930).then(n.t.bind(n,5190,19)),"~blog/default/blog-tags-unit-test-044-list.json",5190],f5222e17:[()=>n.e(7802).then(n.t.bind(n,3868,19)),"~blog/default/blog-tags-express-964-list.json",3868],fc04de90:[()=>n.e(1131).then(n.t.bind(n,2132,19)),"~blog/default/blog-tags-nestjs-fe7-list.json",2132],fd0b92f1:[()=>n.e(7598).then(n.bind(n,5940)),"@site/docs/contribution/vendor-pick-guidelines.md",5940]};function u(e){let{error:t,retry:n,pastDelay:a}=e;return t?r.createElement("div",{style:{textAlign:"center",color:"#fff",backgroundColor:"#fa383e",borderColor:"#fa383e",borderStyle:"solid",borderRadius:"0.25rem",borderWidth:"1px",boxSizing:"border-box",display:"block",padding:"1rem",flex:"0 0 50%",marginLeft:"25%",marginRight:"25%",marginTop:"5rem",maxWidth:"50%",width:"100%"}},r.createElement("p",null,String(t)),r.createElement("div",null,r.createElement("button",{type:"button",onClick:n},"Retry"))):a?r.createElement("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",height:"100vh"}},r.createElement("svg",{id:"loader",style:{width:128,height:110,position:"absolute",top:"calc(100vh - 64%)"},viewBox:"0 0 45 45",xmlns:"http://www.w3.org/2000/svg",stroke:"#61dafb"},r.createElement("g",{fill:"none",fillRule:"evenodd",transform:"translate(1 1)",strokeWidth:"2"},r.createElement("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0"},r.createElement("animate",{attributeName:"r",begin:"1.5s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),r.createElement("animate",{attributeName:"stroke-opacity",begin:"1.5s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),r.createElement("animate",{attributeName:"stroke-width",begin:"1.5s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})),r.createElement("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0"},r.createElement("animate",{attributeName:"r",begin:"3s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),r.createElement("animate",{attributeName:"stroke-opacity",begin:"3s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),r.createElement("animate",{attributeName:"stroke-width",begin:"3s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})),r.createElement("circle",{cx:"22",cy:"22",r:"8"},r.createElement("animate",{attributeName:"r",begin:"0s",dur:"1.5s",values:"6;1;2;3;4;5;6",calcMode:"linear",repeatCount:"indefinite"}))))):null}var c=n(9670),d=n(226);function f(e,t){if("*"===e)return i()({loading:u,loader:()=>n.e(4972).then(n.bind(n,4972)),modules:["@theme/NotFound"],webpack:()=>[4972],render(e,t){const n=e.default;return r.createElement(d.z,{value:{plugin:{name:"native",id:"default"}}},r.createElement(n,t))}});const o=l[`${e}-${t}`],f={},p=[],m=[],g=(0,c.Z)(o);return Object.entries(g).forEach((e=>{let[t,n]=e;const r=s[n];r&&(f[t]=r[0],p.push(r[1]),m.push(r[2]))})),i().Map({loading:u,loader:f,modules:p,webpack:()=>m,render(t,n){const i=JSON.parse(JSON.stringify(o));Object.entries(t).forEach((t=>{let[n,r]=t;const a=r.default;if(!a)throw new Error(`The page component at ${e} doesn't have a default export. This makes it impossible to render anything. Consider default-exporting a React component.`);"object"!=typeof a&&"function"!=typeof a||Object.keys(r).filter((e=>"default"!==e)).forEach((e=>{a[e]=r[e]}));let o=i;const l=n.split(".");l.slice(0,-1).forEach((e=>{o=o[e]})),o[l[l.length-1]]=a}));const l=i.__comp;delete i.__comp;const s=i.__context;return delete i.__context,r.createElement(d.z,{value:s},r.createElement(l,(0,a.Z)({},i,n)))}})}const p=[{path:"/blog",component:f("/blog","112"),exact:!0},{path:"/blog/archive",component:f("/blog/archive","d89"),exact:!0},{path:"/blog/is-prisma-better-than-your-traditional-orm",component:f("/blog/is-prisma-better-than-your-traditional-orm","bb9"),exact:!0},{path:"/blog/monorepo-backend",component:f("/blog/monorepo-backend","4ff"),exact:!0},{path:"/blog/popular-nodejs-pattern-and-tools-to-reconsider",component:f("/blog/popular-nodejs-pattern-and-tools-to-reconsider","250"),exact:!0},{path:"/blog/practica-is-alive",component:f("/blog/practica-is-alive","732"),exact:!0},{path:"/blog/practica-v0.0.6-is-alive",component:f("/blog/practica-v0.0.6-is-alive","5b0"),exact:!0},{path:"/blog/tags",component:f("/blog/tags","71f"),exact:!0},{path:"/blog/tags/component-test",component:f("/blog/tags/component-test","07a"),exact:!0},{path:"/blog/tags/decisions",component:f("/blog/tags/decisions","a64"),exact:!0},{path:"/blog/tags/dotenv",component:f("/blog/tags/dotenv","64d"),exact:!0},{path:"/blog/tags/express",component:f("/blog/tags/express","626"),exact:!0},{path:"/blog/tags/fastify",component:f("/blog/tags/fastify","077"),exact:!0},{path:"/blog/tags/integration",component:f("/blog/tags/integration","efa"),exact:!0},{path:"/blog/tags/monorepo",component:f("/blog/tags/monorepo","c77"),exact:!0},{path:"/blog/tags/nestjs",component:f("/blog/tags/nestjs","12a"),exact:!0},{path:"/blog/tags/nock",component:f("/blog/tags/nock","3e9"),exact:!0},{path:"/blog/tags/node-js",component:f("/blog/tags/node-js","af4"),exact:!0},{path:"/blog/tags/passport",component:f("/blog/tags/passport","31f"),exact:!0},{path:"/blog/tags/practica",component:f("/blog/tags/practica","0c1"),exact:!0},{path:"/blog/tags/prisma",component:f("/blog/tags/prisma","157"),exact:!0},{path:"/blog/tags/supertest",component:f("/blog/tags/supertest","e9c"),exact:!0},{path:"/blog/tags/testing",component:f("/blog/tags/testing","890"),exact:!0},{path:"/blog/tags/unit-test",component:f("/blog/tags/unit-test","18f"),exact:!0},{path:"/blog/testing-the-dark-scenarios-of-your-nodejs-application",component:f("/blog/testing-the-dark-scenarios-of-your-nodejs-application","401"),exact:!0},{path:"/",component:f("/","279"),routes:[{path:"/",component:f("/","a24"),exact:!0,sidebar:"tutorialSidebar"},{path:"/contribution/contribution-long-guide",component:f("/contribution/contribution-long-guide","528"),exact:!0,sidebar:"tutorialSidebar"},{path:"/contribution/contribution-short-guide",component:f("/contribution/contribution-short-guide","cf8"),exact:!0,sidebar:"tutorialSidebar"},{path:"/contribution/release-checklist",component:f("/contribution/release-checklist","7bd"),exact:!0,sidebar:"tutorialSidebar"},{path:"/contribution/vendor-pick-guidelines",component:f("/contribution/vendor-pick-guidelines","ea3"),exact:!0,sidebar:"tutorialSidebar"},{path:"/decisions/",component:f("/decisions/","f73"),exact:!0,sidebar:"tutorialSidebar"},{path:"/decisions/configuration-library",component:f("/decisions/configuration-library","c9f"),exact:!0,sidebar:"tutorialSidebar"},{path:"/decisions/docker-base-image",component:f("/decisions/docker-base-image","c98"),exact:!0,sidebar:"tutorialSidebar"},{path:"/decisions/monorepo",component:f("/decisions/monorepo","ebc"),exact:!0,sidebar:"tutorialSidebar"},{path:"/decisions/openapi",component:f("/decisions/openapi","4f3"),exact:!0,sidebar:"tutorialSidebar"},{path:"/features",component:f("/features","beb"),exact:!0,sidebar:"tutorialSidebar"},{path:"/questions",component:f("/questions","1c5"),exact:!0,sidebar:"tutorialSidebar"},{path:"/the-basics/coding-with-practica",component:f("/the-basics/coding-with-practica","d23"),exact:!0,sidebar:"tutorialSidebar"},{path:"/the-basics/getting-started-quickly",component:f("/the-basics/getting-started-quickly","c4e"),exact:!0,sidebar:"tutorialSidebar"},{path:"/the-basics/what-is-practica",component:f("/the-basics/what-is-practica","d24"),exact:!0,sidebar:"tutorialSidebar"}]},{path:"*",component:f("*")}]},8934:(e,t,n)=>{"use strict";n.d(t,{_:()=>a,t:()=>o});var r=n(7294);const a=r.createContext(!1);function o(e){let{children:t}=e;const[n,o]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{o(!0)}),[]),r.createElement(a.Provider,{value:n},t)}},9383:(e,t,n)=>{"use strict";var r=n(7294),a=n(3935),o=n(3727),i=n(405),l=n(412);const s=[n(4367),n(2497),n(3310),n(8320),n(2295)];var u=n(723),c=n(6775),d=n(8790);function f(e){let{children:t}=e;return r.createElement(r.Fragment,null,t)}var p=n(7462),m=n(5742),g=n(2263),h=n(4996),b=n(6668),v=n(1944),y=n(4711),w=n(9727),k=n(3320),E=n(197);function S(){const{i18n:{defaultLocale:e,localeConfigs:t}}=(0,g.Z)(),n=(0,y.l)();return r.createElement(m.Z,null,Object.entries(t).map((e=>{let[t,{htmlLang:a}]=e;return r.createElement("link",{key:t,rel:"alternate",href:n.createUrl({locale:t,fullyQualified:!0}),hrefLang:a})})),r.createElement("link",{rel:"alternate",href:n.createUrl({locale:e,fullyQualified:!0}),hrefLang:"x-default"}))}function x(e){let{permalink:t}=e;const{siteConfig:{url:n}}=(0,g.Z)(),a=function(){const{siteConfig:{url:e}}=(0,g.Z)(),{pathname:t}=(0,c.TH)();return e+(0,h.Z)(t)}(),o=t?`${n}${t}`:a;return r.createElement(m.Z,null,r.createElement("meta",{property:"og:url",content:o}),r.createElement("link",{rel:"canonical",href:o}))}function _(){const{i18n:{currentLocale:e}}=(0,g.Z)(),{metadata:t,image:n}=(0,b.L)();return r.createElement(r.Fragment,null,r.createElement(m.Z,null,r.createElement("meta",{name:"twitter:card",content:"summary_large_image"}),r.createElement("body",{className:w.h})),n&&r.createElement(v.d,{image:n}),r.createElement(x,null),r.createElement(S,null),r.createElement(E.Z,{tag:k.HX,locale:e}),r.createElement(m.Z,null,t.map(((e,t)=>r.createElement("meta",(0,p.Z)({key:t},e))))))}const C=new Map;function T(e){if(C.has(e.pathname))return{...e,pathname:C.get(e.pathname)};if((0,d.f)(u.Z,e.pathname).some((e=>{let{route:t}=e;return!0===t.exact})))return C.set(e.pathname,e.pathname),e;const t=e.pathname.trim().replace(/(?:\/index)?\.html$/,"")||"/";return C.set(e.pathname,t),{...e,pathname:t}}var A=n(8934),L=n(8940);function R(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r{var r;const a=(null==(r=t.default)?void 0:r[e])??t[e];return null==a?void 0:a(...n)}));return()=>a.forEach((e=>null==e?void 0:e()))}const P=function(e){let{children:t,location:n,previousLocation:a}=e;return(0,r.useLayoutEffect)((()=>{a!==n&&(a&&function(e){const{hash:t}=e;if(t){const e=decodeURIComponent(t.substring(1)),n=document.getElementById(e);null==n||n.scrollIntoView()}else window.scrollTo(0,0)}(n),R("onRouteDidUpdate",{previousLocation:a,location:n}))}),[a,n]),t};function N(e){const t=Array.from(new Set([e,decodeURI(e)])).map((e=>(0,d.f)(u.Z,e))).flat();return Promise.all(t.map((e=>null==e.route.component.preload?void 0:e.route.component.preload())))}class O extends r.Component{constructor(e){super(e),this.previousLocation=void 0,this.routeUpdateCleanupCb=void 0,this.previousLocation=null,this.routeUpdateCleanupCb=l.Z.canUseDOM?R("onRouteUpdate",{previousLocation:null,location:this.props.location}):()=>{},this.state={nextRouteHasLoaded:!0}}shouldComponentUpdate(e,t){if(e.location===this.props.location)return t.nextRouteHasLoaded;const n=e.location;return this.previousLocation=this.props.location,this.setState({nextRouteHasLoaded:!1}),this.routeUpdateCleanupCb=R("onRouteUpdate",{previousLocation:this.previousLocation,location:n}),N(n.pathname).then((()=>{this.routeUpdateCleanupCb(),this.setState({nextRouteHasLoaded:!0})})).catch((e=>{console.warn(e),window.location.reload()})),!1}render(){const{children:e,location:t}=this.props;return r.createElement(P,{previousLocation:this.previousLocation,location:t},r.createElement(c.AW,{location:t,render:()=>e}))}}const D=O,I="docusaurus-base-url-issue-banner-container",M="docusaurus-base-url-issue-banner-suggestion-container",j="__DOCUSAURUS_INSERT_BASEURL_BANNER";function F(e){return`\nwindow['${j}'] = true;\n\ndocument.addEventListener('DOMContentLoaded', maybeInsertBanner);\n\nfunction maybeInsertBanner() {\n var shouldInsert = window['${j}'];\n shouldInsert && insertBanner();\n}\n\nfunction insertBanner() {\n var bannerContainer = document.getElementById('${I}');\n if (!bannerContainer) {\n return;\n }\n var bannerHtml = ${JSON.stringify(function(e){return`\n
\n

Your Docusaurus site did not load properly.

\n

A very common reason is a wrong site baseUrl configuration.

\n

Current configured baseUrl = ${e} ${"/"===e?" (default value)":""}

\n

We suggest trying baseUrl =

\n
\n`}(e)).replace(/{window[j]=!1}),[]),r.createElement(r.Fragment,null,!l.Z.canUseDOM&&r.createElement(m.Z,null,r.createElement("script",null,F(e))),r.createElement("div",{id:I}))}function z(){const{siteConfig:{baseUrl:e,baseUrlIssueBanner:t}}=(0,g.Z)(),{pathname:n}=(0,c.TH)();return t&&n===e?r.createElement(B,null):null}function U(){const{siteConfig:{favicon:e,title:t,noIndex:n},i18n:{currentLocale:a,localeConfigs:o}}=(0,g.Z)(),i=(0,h.Z)(e),{htmlLang:l,direction:s}=o[a];return r.createElement(m.Z,null,r.createElement("html",{lang:l,dir:s}),r.createElement("title",null,t),r.createElement("meta",{property:"og:title",content:t}),r.createElement("meta",{name:"viewport",content:"width=device-width, initial-scale=1.0"}),n&&r.createElement("meta",{name:"robots",content:"noindex, nofollow"}),e&&r.createElement("link",{rel:"icon",href:i}))}var $=n(4763);function q(){const e=(0,d.H)(u.Z),t=(0,c.TH)();return r.createElement($.Z,null,r.createElement(L.M,null,r.createElement(A.t,null,r.createElement(f,null,r.createElement(U,null),r.createElement(_,null),r.createElement(z,null),r.createElement(D,{location:T(t)},e)))))}var G=n(6887);const H=function(e){try{return document.createElement("link").relList.supports(e)}catch{return!1}}("prefetch")?function(e){return new Promise(((t,n)=>{var r;if("undefined"==typeof document)return void n();const a=document.createElement("link");a.setAttribute("rel","prefetch"),a.setAttribute("href",e),a.onload=()=>t(),a.onerror=()=>n();const o=document.getElementsByTagName("head")[0]??(null==(r=document.getElementsByName("script")[0])?void 0:r.parentNode);null==o||o.appendChild(a)}))}:function(e){return new Promise(((t,n)=>{const r=new XMLHttpRequest;r.open("GET",e,!0),r.withCredentials=!0,r.onload=()=>{200===r.status?t():n()},r.send(null)}))};var Z=n(9670);const V=new Set,W=new Set,Y=()=>{var e,t;return(null==(e=navigator.connection)?void 0:e.effectiveType.includes("2g"))||(null==(t=navigator.connection)?void 0:t.saveData)},K={prefetch(e){if(!(e=>!Y()&&!W.has(e)&&!V.has(e))(e))return!1;V.add(e);const t=(0,d.f)(u.Z,e).flatMap((e=>{return t=e.route.path,Object.entries(G).filter((e=>{let[n]=e;return n.replace(/-[^-]+$/,"")===t})).flatMap((e=>{let[,t]=e;return Object.values((0,Z.Z)(t))}));var t}));return Promise.all(t.map((e=>{const t=n.gca(e);return t&&!t.includes("undefined")?H(t).catch((()=>{})):Promise.resolve()})))},preload:e=>!!(e=>!Y()&&!W.has(e))(e)&&(W.add(e),N(e))},Q=Object.freeze(K);if(l.Z.canUseDOM){window.docusaurus=Q;const e=a.hydrate;N(window.location.pathname).then((()=>{e(r.createElement(i.B6,null,r.createElement(o.VK,null,r.createElement(q,null))),document.getElementById("__docusaurus"))}))}},8940:(e,t,n)=>{"use strict";n.d(t,{_:()=>c,M:()=>d});var r=n(7294),a=n(6809);const o=JSON.parse('{"docusaurus-plugin-content-docs":{"default":{"path":"/","versions":[{"name":"current","label":"Next","isLast":true,"path":"/","mainDocId":"home","docs":[{"id":"contribution/contribution-long-guide","path":"/contribution/contribution-long-guide","sidebar":"tutorialSidebar"},{"id":"contribution/contribution-short-guide","path":"/contribution/contribution-short-guide","sidebar":"tutorialSidebar"},{"id":"contribution/release-checklist","path":"/contribution/release-checklist","sidebar":"tutorialSidebar"},{"id":"contribution/vendor-pick-guidelines","path":"/contribution/vendor-pick-guidelines","sidebar":"tutorialSidebar"},{"id":"decisions/configuration-library","path":"/decisions/configuration-library","sidebar":"tutorialSidebar"},{"id":"decisions/docker-base-image","path":"/decisions/docker-base-image","sidebar":"tutorialSidebar"},{"id":"decisions/index","path":"/decisions/","sidebar":"tutorialSidebar"},{"id":"decisions/monorepo","path":"/decisions/monorepo","sidebar":"tutorialSidebar"},{"id":"decisions/openapi","path":"/decisions/openapi","sidebar":"tutorialSidebar"},{"id":"features","path":"/features","sidebar":"tutorialSidebar"},{"id":"home","path":"/","sidebar":"tutorialSidebar"},{"id":"questions","path":"/questions","sidebar":"tutorialSidebar"},{"id":"the-basics/coding-with-practica","path":"/the-basics/coding-with-practica","sidebar":"tutorialSidebar"},{"id":"the-basics/getting-started-quickly","path":"/the-basics/getting-started-quickly","sidebar":"tutorialSidebar"},{"id":"the-basics/what-is-practica","path":"/the-basics/what-is-practica","sidebar":"tutorialSidebar"}],"draftIds":[],"sidebars":{"tutorialSidebar":{"link":{"path":"/","label":"home"}}}}],"breadcrumbs":true}}}'),i=JSON.parse('{"defaultLocale":"en","locales":["en"],"path":"i18n","currentLocale":"en","localeConfigs":{"en":{"label":"English","direction":"ltr","htmlLang":"en","calendar":"gregory","path":"en"}}}');var l=n(7529);const s=JSON.parse('{"docusaurusVersion":"2.1.0","siteVersion":"0.0.0","pluginVersions":{"docusaurus-plugin-content-docs":{"type":"package","name":"@docusaurus/plugin-content-docs","version":"2.1.0"},"docusaurus-plugin-content-blog":{"type":"package","name":"@docusaurus/plugin-content-blog","version":"2.1.0"},"docusaurus-plugin-content-pages":{"type":"package","name":"@docusaurus/plugin-content-pages","version":"2.1.0"},"docusaurus-plugin-google-analytics":{"type":"package","name":"@docusaurus/plugin-google-analytics","version":"2.1.0"},"docusaurus-plugin-sitemap":{"type":"package","name":"@docusaurus/plugin-sitemap","version":"2.1.0"},"docusaurus-theme-classic":{"type":"package","name":"@docusaurus/theme-classic","version":"2.1.0"}}}'),u={siteConfig:a.Z,siteMetadata:s,globalData:o,i18n:i,codeTranslations:l},c=r.createContext(u);function d(e){let{children:t}=e;return r.createElement(c.Provider,{value:u},t)}},4763:(e,t,n)=>{"use strict";n.d(t,{Z:()=>c});var r=n(7294),a=n(412),o=n(5742),i=n(215);function l(e){let{error:t,tryAgain:n}=e;return r.createElement("div",{style:{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"center",height:"50vh",width:"100%",fontSize:"20px"}},r.createElement("h1",null,"This page crashed."),r.createElement("p",null,t.message),r.createElement("button",{type:"button",onClick:n},"Try again"))}function s(e){let{error:t,tryAgain:n}=e;return r.createElement(c,{fallback:()=>r.createElement(l,{error:t,tryAgain:n})},r.createElement(o.Z,null,r.createElement("title",null,"Page Error")),r.createElement(i.Z,null,r.createElement(l,{error:t,tryAgain:n})))}const u=e=>r.createElement(s,e);class c extends r.Component{constructor(e){super(e),this.state={error:null}}componentDidCatch(e){a.Z.canUseDOM&&this.setState({error:e})}render(){const{children:e}=this.props,{error:t}=this.state;if(t){const e={error:t,tryAgain:()=>this.setState({error:null})};return(this.props.fallback??u)(e)}return e??null}}},412:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});const r="undefined"!=typeof window&&"document"in window&&"createElement"in window.document,a={canUseDOM:r,canUseEventListeners:r&&("addEventListener"in window||"attachEvent"in window),canUseIntersectionObserver:r&&"IntersectionObserver"in window,canUseViewport:r&&"screen"in window}},5742:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});var r=n(7294),a=n(405);function o(e){return r.createElement(a.ql,e)}},9960:(e,t,n)=>{"use strict";n.d(t,{Z:()=>p});var r=n(7462),a=n(7294),o=n(3727),i=n(8780),l=n(2263),s=n(3919),u=n(412);const c=a.createContext({collectLink:()=>{}});var d=n(4996);function f(e,t){var n;let{isNavLink:f,to:p,href:m,activeClassName:g,isActive:h,"data-noBrokenLinkCheck":b,autoAddBaseUrl:v=!0,...y}=e;const{siteConfig:{trailingSlash:w,baseUrl:k}}=(0,l.Z)(),{withBaseUrl:E}=(0,d.C)(),S=(0,a.useContext)(c),x=(0,a.useRef)(null);(0,a.useImperativeHandle)(t,(()=>x.current));const _=p||m;const C=(0,s.Z)(_),T=null==_?void 0:_.replace("pathname://","");let A=void 0!==T?(L=T,v&&(e=>e.startsWith("/"))(L)?E(L):L):void 0;var L;A&&C&&(A=(0,i.applyTrailingSlash)(A,{trailingSlash:w,baseUrl:k}));const R=(0,a.useRef)(!1),P=f?o.OL:o.rU,N=u.Z.canUseIntersectionObserver,O=(0,a.useRef)();(0,a.useEffect)((()=>(!N&&C&&null!=A&&window.docusaurus.prefetch(A),()=>{N&&O.current&&O.current.disconnect()})),[O,A,N,C]);const D=(null==(n=A)?void 0:n.startsWith("#"))??!1,I=!A||!C||D;return I||b||S.collectLink(A),I?a.createElement("a",(0,r.Z)({ref:x,href:A},_&&!C&&{target:"_blank",rel:"noopener noreferrer"},y)):a.createElement(P,(0,r.Z)({},y,{onMouseEnter:()=>{R.current||null==A||(window.docusaurus.preload(A),R.current=!0)},innerRef:e=>{x.current=e,N&&e&&C&&(O.current=new window.IntersectionObserver((t=>{t.forEach((t=>{e===t.target&&(t.isIntersecting||t.intersectionRatio>0)&&(O.current.unobserve(e),O.current.disconnect(),null!=A&&window.docusaurus.prefetch(A))}))})),O.current.observe(e))},to:A},f&&{isActive:h,activeClassName:g}))}const p=a.forwardRef(f)},5999:(e,t,n)=>{"use strict";n.d(t,{Z:()=>s,I:()=>l});var r=n(7294);function a(e,t){const n=e.split(/(\{\w+\})/).map(((e,n)=>{if(n%2==1){const n=null==t?void 0:t[e.slice(1,-1)];if(void 0!==n)return n}return e}));return n.some((e=>(0,r.isValidElement)(e)))?n.map(((e,t)=>(0,r.isValidElement)(e)?r.cloneElement(e,{key:t}):e)).filter((e=>""!==e)):n.join("")}var o=n(7529);function i(e){let{id:t,message:n}=e;if(void 0===t&&void 0===n)throw new Error("Docusaurus translation declarations must have at least a translation id or a default translation message");return o[t??n]??n??t}function l(e,t){let{message:n,id:r}=e;return a(i({message:n,id:r}),t)}function s(e){let{children:t,id:n,values:o}=e;if(t&&"string"!=typeof t)throw console.warn("Illegal children",t),new Error("The Docusaurus component only accept simple string values");const l=i({message:t,id:n});return r.createElement(r.Fragment,null,a(l,o))}},9935:(e,t,n)=>{"use strict";n.d(t,{m:()=>r});const r="default"},3919:(e,t,n)=>{"use strict";function r(e){return/^(?:\w*:|\/\/)/.test(e)}function a(e){return void 0!==e&&!r(e)}n.d(t,{Z:()=>a,b:()=>r})},4996:(e,t,n)=>{"use strict";n.d(t,{C:()=>o,Z:()=>i});var r=n(2263),a=n(3919);function o(){const{siteConfig:{baseUrl:e,url:t}}=(0,r.Z)();return{withBaseUrl:(n,r)=>function(e,t,n,r){let{forcePrependBaseUrl:o=!1,absolute:i=!1}=void 0===r?{}:r;if(!n||n.startsWith("#")||(0,a.b)(n))return n;if(o)return t+n.replace(/^\//,"");if(n===t.replace(/\/$/,""))return t;const l=n.startsWith(t)?n:t+n.replace(/^\//,"");return i?e+l:l}(t,e,n,r)}}function i(e,t){void 0===t&&(t={});const{withBaseUrl:n}=o();return n(e,t)}},2263:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});var r=n(7294),a=n(8940);function o(){return(0,r.useContext)(a._)}},2389:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});var r=n(7294),a=n(8934);function o(){return(0,r.useContext)(a._)}},9670:(e,t,n)=>{"use strict";n.d(t,{Z:()=>r});function r(e){const t={};return function e(n,r){Object.entries(n).forEach((n=>{let[a,o]=n;const i=r?`${r}.${a}`:a;var l;"object"==typeof(l=o)&&l&&Object.keys(l).length>0?e(o,i):t[i]=o}))}(e),t}},226:(e,t,n)=>{"use strict";n.d(t,{_:()=>a,z:()=>o});var r=n(7294);const a=r.createContext(null);function o(e){let{children:t,value:n}=e;const o=r.useContext(a),i=(0,r.useMemo)((()=>function(e){let{parent:t,value:n}=e;if(!t){if(!n)throw new Error("Unexpected: no Docusaurus route context found");if(!("plugin"in n))throw new Error("Unexpected: Docusaurus topmost route context has no `plugin` attribute");return n}const r={...t.data,...null==n?void 0:n.data};return{plugin:t.plugin,data:r}}({parent:o,value:n})),[o,n]);return r.createElement(a.Provider,{value:i},t)}},143:(e,t,n)=>{"use strict";n.d(t,{Iw:()=>g,gA:()=>f,_r:()=>c,Jo:()=>h,zh:()=>d,yW:()=>m,gB:()=>p});var r=n(6775),a=n(2263),o=n(9935);function i(e,t){void 0===t&&(t={});const n=function(){const{globalData:e}=(0,a.Z)();return e}()[e];if(!n&&t.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin.`);return n}const l=e=>e.versions.find((e=>e.isLast));function s(e,t){const n=function(e,t){const n=l(e);return[...e.versions.filter((e=>e!==n)),n].find((e=>!!(0,r.LX)(t,{path:e.path,exact:!1,strict:!1})))}(e,t),a=null==n?void 0:n.docs.find((e=>!!(0,r.LX)(t,{path:e.path,exact:!0,strict:!1})));return{activeVersion:n,activeDoc:a,alternateDocVersions:a?function(t){const n={};return e.versions.forEach((e=>{e.docs.forEach((r=>{r.id===t&&(n[e.name]=r)}))})),n}(a.id):{}}}const u={},c=()=>i("docusaurus-plugin-content-docs")??u,d=e=>function(e,t,n){void 0===t&&(t=o.m),void 0===n&&(n={});const r=i(e),a=null==r?void 0:r[t];if(!a&&n.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin with id "${t}".`);return a}("docusaurus-plugin-content-docs",e,{failfast:!0});function f(e){void 0===e&&(e={});const t=c(),{pathname:n}=(0,r.TH)();return function(e,t,n){void 0===n&&(n={});const a=Object.entries(e).sort(((e,t)=>t[1].path.localeCompare(e[1].path))).find((e=>{let[,n]=e;return!!(0,r.LX)(t,{path:n.path,exact:!1,strict:!1})})),o=a?{pluginId:a[0],pluginData:a[1]}:void 0;if(!o&&n.failfast)throw new Error(`Can't find active docs plugin for "${t}" pathname, while it was expected to be found. Maybe you tried to use a docs feature that can only be used on a docs-related page? Existing docs plugin paths are: ${Object.values(e).map((e=>e.path)).join(", ")}`);return o}(t,n,e)}function p(e){return d(e).versions}function m(e){const t=d(e);return l(t)}function g(e){const t=d(e),{pathname:n}=(0,r.TH)();return s(t,n)}function h(e){const t=d(e),{pathname:n}=(0,r.TH)();return function(e,t){const n=l(e);return{latestDocSuggestion:s(e,t).alternateDocVersions[n.name],latestVersionSuggestion:n}}(t,n)}},4367:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r={onRouteDidUpdate(e){let{location:t,previousLocation:n}=e;!n||t.pathname===n.pathname&&t.search===n.search&&t.hash===n.hash||(window.ga("set","page",t.pathname+t.search+t.hash),window.ga("send","pageview"))}}},8320:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>o});var r=n(4865),a=n.n(r);a().configure({showSpinner:!1});const o={onRouteUpdate(e){let{location:t,previousLocation:n}=e;if(n&&t.pathname!==n.pathname){const e=window.setTimeout((()=>{a().start()}),200);return()=>window.clearTimeout(e)}},onRouteDidUpdate(){a().done()}}},3310:(e,t,n)=>{"use strict";n.r(t);var r=n(7410),a=n(6809);!function(e){const{themeConfig:{prism:t}}=a.Z,{additionalLanguages:r}=t;globalThis.Prism=e,r.forEach((e=>{n(6726)(`./prism-${e}`)})),delete globalThis.Prism}(r.Z)},9471:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});var r=n(7294);const a="iconExternalLink_nPIU";function o(e){let{width:t=13.5,height:n=13.5}=e;return r.createElement("svg",{width:t,height:n,"aria-hidden":"true",viewBox:"0 0 24 24",className:a},r.createElement("path",{fill:"currentColor",d:"M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"}))}},215:(e,t,n)=>{"use strict";n.d(t,{Z:()=>lt});var r=n(7294),a=n(6010),o=n(4763),i=n(1944),l=n(5281),s=n(9727),u=n(5999),c=n(6775),d=n(5936);function f(e){e.setAttribute("tabindex","-1"),e.focus(),e.removeAttribute("tabindex")}const p="skipToContent_fXgn";function m(){const{containerRef:e,handleSkip:t}=function(){const e=(0,r.useRef)(null),{action:t}=(0,c.k6)(),n=(0,r.useCallback)((e=>{e.preventDefault();const t=document.querySelector("main:first-of-type")??document.querySelector(`.${l.k.wrapper.main}`);t&&f(t)}),[]);return(0,d.S)((n=>{let{location:r}=n;e.current&&!r.hash&&"PUSH"===t&&f(e.current)})),{containerRef:e,handleSkip:n}}();return r.createElement("div",{ref:e,role:"region","aria-label":(0,u.I)({id:"theme.common.skipToMainContent"})},r.createElement("a",{href:"#",className:p,onClick:t},r.createElement(u.Z,{id:"theme.common.skipToMainContent",description:"The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation"},"Skip to main content")))}var g=n(6668),h=n(9689),b=n(7462);function v(e){let{width:t=21,height:n=21,color:a="currentColor",strokeWidth:o=1.2,className:i,...l}=e;return r.createElement("svg",(0,b.Z)({viewBox:"0 0 15 15",width:t,height:n},l),r.createElement("g",{stroke:a,strokeWidth:o},r.createElement("path",{d:"M.75.75l13.5 13.5M14.25.75L.75 14.25"})))}const y="closeButton_CVFx";function w(e){return r.createElement("button",(0,b.Z)({type:"button","aria-label":(0,u.I)({id:"theme.AnnouncementBar.closeButtonAriaLabel",message:"Close",description:"The ARIA label for close button of announcement bar"})},e,{className:(0,a.Z)("clean-btn close",y,e.className)}),r.createElement(v,{width:14,height:14,strokeWidth:3.1}))}const k="content_knG7";function E(e){const{announcementBar:t}=(0,g.L)(),{content:n}=t;return r.createElement("div",(0,b.Z)({},e,{className:(0,a.Z)(k,e.className),dangerouslySetInnerHTML:{__html:n}}))}const S="announcementBar_mb4j",x="announcementBarPlaceholder_vyr4",_="announcementBarClose_gvF7",C="announcementBarContent_xLdY";function T(){const{announcementBar:e}=(0,g.L)(),{isActive:t,close:n}=(0,h.nT)();if(!t)return null;const{backgroundColor:a,textColor:o,isCloseable:i}=e;return r.createElement("div",{className:S,style:{backgroundColor:a,color:o},role:"banner"},i&&r.createElement("div",{className:x}),r.createElement(E,{className:C}),i&&r.createElement(w,{onClick:n,className:_}))}var A=n(2961),L=n(2466);var R=n(902),P=n(3102);const N=r.createContext(null);function O(e){let{children:t}=e;const n=function(){const e=(0,A.e)(),t=(0,P.HY)(),[n,a]=(0,r.useState)(!1),o=null!==t.component,i=(0,R.D9)(o);return(0,r.useEffect)((()=>{o&&!i&&a(!0)}),[o,i]),(0,r.useEffect)((()=>{o?e.shown||a(!0):a(!1)}),[e.shown,o]),(0,r.useMemo)((()=>[n,a]),[n])}();return r.createElement(N.Provider,{value:n},t)}function D(e){if(e.component){const t=e.component;return r.createElement(t,e.props)}}function I(){const e=(0,r.useContext)(N);if(!e)throw new R.i6("NavbarSecondaryMenuDisplayProvider");const[t,n]=e,a=(0,r.useCallback)((()=>n(!1)),[n]),o=(0,P.HY)();return(0,r.useMemo)((()=>({shown:t,hide:a,content:D(o)})),[a,o,t])}function M(e){let{header:t,primaryMenu:n,secondaryMenu:o}=e;const{shown:i}=I();return r.createElement("div",{className:"navbar-sidebar"},t,r.createElement("div",{className:(0,a.Z)("navbar-sidebar__items",{"navbar-sidebar__items--show-secondary":i})},r.createElement("div",{className:"navbar-sidebar__item menu"},n),r.createElement("div",{className:"navbar-sidebar__item menu"},o)))}var j=n(2949),F=n(2389);function B(e){return r.createElement("svg",(0,b.Z)({viewBox:"0 0 24 24",width:24,height:24},e),r.createElement("path",{fill:"currentColor",d:"M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"}))}function z(e){return r.createElement("svg",(0,b.Z)({viewBox:"0 0 24 24",width:24,height:24},e),r.createElement("path",{fill:"currentColor",d:"M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"}))}const U={toggle:"toggle_vylO",toggleButton:"toggleButton_gllP",darkToggleIcon:"darkToggleIcon_wfgR",lightToggleIcon:"lightToggleIcon_pyhR",toggleButtonDisabled:"toggleButtonDisabled_aARS"};function $(e){let{className:t,value:n,onChange:o}=e;const i=(0,F.Z)(),l=(0,u.I)({message:"Switch between dark and light mode (currently {mode})",id:"theme.colorToggle.ariaLabel",description:"The ARIA label for the navbar color mode toggle"},{mode:"dark"===n?(0,u.I)({message:"dark mode",id:"theme.colorToggle.ariaLabel.mode.dark",description:"The name for the dark color mode"}):(0,u.I)({message:"light mode",id:"theme.colorToggle.ariaLabel.mode.light",description:"The name for the light color mode"})});return r.createElement("div",{className:(0,a.Z)(U.toggle,t)},r.createElement("button",{className:(0,a.Z)("clean-btn",U.toggleButton,!i&&U.toggleButtonDisabled),type:"button",onClick:()=>o("dark"===n?"light":"dark"),disabled:!i,title:l,"aria-label":l},r.createElement(B,{className:(0,a.Z)(U.toggleIcon,U.lightToggleIcon)}),r.createElement(z,{className:(0,a.Z)(U.toggleIcon,U.darkToggleIcon)})))}const q=r.memo($);function G(e){let{className:t}=e;const n=(0,g.L)().colorMode.disableSwitch,{colorMode:a,setColorMode:o}=(0,j.I)();return n?null:r.createElement(q,{className:t,value:a,onChange:o})}var H=n(1327);function Z(){return r.createElement(H.Z,{className:"navbar__brand",imageClassName:"navbar__logo",titleClassName:"navbar__title text--truncate"})}function V(){const e=(0,A.e)();return r.createElement("button",{type:"button",className:"clean-btn navbar-sidebar__close",onClick:()=>e.toggle()},r.createElement(v,{color:"var(--ifm-color-emphasis-600)"}))}function W(){return r.createElement("div",{className:"navbar-sidebar__brand"},r.createElement(Z,null),r.createElement(G,{className:"margin-right--md"}),r.createElement(V,null))}var Y=n(9960),K=n(4996),Q=n(3919);function X(e,t){return void 0!==e&&void 0!==t&&new RegExp(e,"gi").test(t)}var J=n(9471);function ee(e){let{activeBasePath:t,activeBaseRegex:n,to:a,href:o,label:i,html:l,isDropdownLink:s,prependBaseUrlToHref:u,...c}=e;const d=(0,K.Z)(a),f=(0,K.Z)(t),p=(0,K.Z)(o,{forcePrependBaseUrl:!0}),m=i&&o&&!(0,Q.Z)(o),g=l?{dangerouslySetInnerHTML:{__html:l}}:{children:r.createElement(r.Fragment,null,i,m&&r.createElement(J.Z,s&&{width:12,height:12}))};return o?r.createElement(Y.Z,(0,b.Z)({href:u?p:o},c,g)):r.createElement(Y.Z,(0,b.Z)({to:d,isNavLink:!0},(t||n)&&{isActive:(e,t)=>n?X(n,t.pathname):t.pathname.startsWith(f)},c,g))}function te(e){let{className:t,isDropdownItem:n=!1,...o}=e;const i=r.createElement(ee,(0,b.Z)({className:(0,a.Z)(n?"dropdown__link":"navbar__item navbar__link",t),isDropdownLink:n},o));return n?r.createElement("li",null,i):i}function ne(e){let{className:t,isDropdownItem:n,...o}=e;return r.createElement("li",{className:"menu__list-item"},r.createElement(ee,(0,b.Z)({className:(0,a.Z)("menu__link",t)},o)))}function re(e){let{mobile:t=!1,position:n,...a}=e;const o=t?ne:te;return r.createElement(o,(0,b.Z)({},a,{activeClassName:a.activeClassName??(t?"menu__link--active":"navbar__link--active")}))}var ae=n(6043),oe=n(8596),ie=n(2263);function le(e,t){return e.some((e=>function(e,t){return!!(0,oe.Mg)(e.to,t)||!!X(e.activeBaseRegex,t)||!(!e.activeBasePath||!t.startsWith(e.activeBasePath))}(e,t)))}function se(e){let{items:t,position:n,className:o,onClick:i,...l}=e;const s=(0,r.useRef)(null),[u,c]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{const e=e=>{s.current&&!s.current.contains(e.target)&&c(!1)};return document.addEventListener("mousedown",e),document.addEventListener("touchstart",e),()=>{document.removeEventListener("mousedown",e),document.removeEventListener("touchstart",e)}}),[s]),r.createElement("div",{ref:s,className:(0,a.Z)("navbar__item","dropdown","dropdown--hoverable",{"dropdown--right":"right"===n,"dropdown--show":u})},r.createElement(ee,(0,b.Z)({"aria-haspopup":"true","aria-expanded":u,role:"button",href:l.to?void 0:"#",className:(0,a.Z)("navbar__link",o)},l,{onClick:l.to?void 0:e=>e.preventDefault(),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),c(!u))}}),l.children??l.label),r.createElement("ul",{className:"dropdown__menu"},t.map(((e,n)=>r.createElement(Ee,(0,b.Z)({isDropdownItem:!0,onKeyDown:e=>{if(n===t.length-1&&"Tab"===e.key){e.preventDefault(),c(!1);const t=s.current.nextElementSibling;if(t){(t instanceof HTMLAnchorElement?t:t.querySelector("a")).focus()}}},activeClassName:"dropdown__link--active"},e,{key:n}))))))}function ue(e){let{items:t,className:n,position:o,onClick:i,...l}=e;const s=function(){const{siteConfig:{baseUrl:e}}=(0,ie.Z)(),{pathname:t}=(0,c.TH)();return t.replace(e,"/")}(),u=le(t,s),{collapsed:d,toggleCollapsed:f,setCollapsed:p}=(0,ae.u)({initialState:()=>!u});return(0,r.useEffect)((()=>{u&&p(!u)}),[s,u,p]),r.createElement("li",{className:(0,a.Z)("menu__list-item",{"menu__list-item--collapsed":d})},r.createElement(ee,(0,b.Z)({role:"button",className:(0,a.Z)("menu__link menu__link--sublist menu__link--sublist-caret",n)},l,{onClick:e=>{e.preventDefault(),f()}}),l.children??l.label),r.createElement(ae.z,{lazy:!0,as:"ul",className:"menu__list",collapsed:d},t.map(((e,t)=>r.createElement(Ee,(0,b.Z)({mobile:!0,isDropdownItem:!0,onClick:i,activeClassName:"menu__link--active"},e,{key:t}))))))}function ce(e){let{mobile:t=!1,...n}=e;const a=t?ue:se;return r.createElement(a,n)}var de=n(4711);function fe(e){let{width:t=20,height:n=20,...a}=e;return r.createElement("svg",(0,b.Z)({viewBox:"0 0 24 24",width:t,height:n,"aria-hidden":!0},a),r.createElement("path",{fill:"currentColor",d:"M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"}))}const pe="iconLanguage_nlXk";const me=()=>null,ge="searchBox_ZlJk";function he(e){let{children:t,className:n}=e;return r.createElement("div",{className:(0,a.Z)(n,ge)},t)}var be=n(143),ve=n(2802);var ye=n(373);const we=e=>e.docs.find((t=>t.id===e.mainDocId));const ke={default:re,localeDropdown:function(e){let{mobile:t,dropdownItemsBefore:n,dropdownItemsAfter:a,...o}=e;const{i18n:{currentLocale:i,locales:l,localeConfigs:s}}=(0,ie.Z)(),c=(0,de.l)(),d=[...n,...l.map((e=>{const n=`pathname://${c.createUrl({locale:e,fullyQualified:!1})}`;return{label:s[e].label,lang:s[e].htmlLang,to:n,target:"_self",autoAddBaseUrl:!1,className:e===i?t?"menu__link--active":"dropdown__link--active":""}})),...a],f=t?(0,u.I)({message:"Languages",id:"theme.navbar.mobileLanguageDropdown.label",description:"The label for the mobile language switcher dropdown"}):s[i].label;return r.createElement(ce,(0,b.Z)({},o,{mobile:t,label:r.createElement(r.Fragment,null,r.createElement(fe,{className:pe}),f),items:d}))},search:function(e){let{mobile:t,className:n}=e;return t?null:r.createElement(he,{className:n},r.createElement(me,null))},dropdown:ce,html:function(e){let{value:t,className:n,mobile:o=!1,isDropdownItem:i=!1}=e;const l=i?"li":"div";return r.createElement(l,{className:(0,a.Z)({navbar__item:!o&&!i,"menu__list-item":o},n),dangerouslySetInnerHTML:{__html:t}})},doc:function(e){let{docId:t,label:n,docsPluginId:a,...o}=e;const{activeDoc:i}=(0,be.Iw)(a),l=(0,ve.vY)(t,a);return null===l?null:r.createElement(re,(0,b.Z)({exact:!0},o,{isActive:()=>(null==i?void 0:i.path)===l.path||!(null==i||!i.sidebar)&&i.sidebar===l.sidebar,label:n??l.id,to:l.path}))},docSidebar:function(e){let{sidebarId:t,label:n,docsPluginId:a,...o}=e;const{activeDoc:i}=(0,be.Iw)(a),l=(0,ve.oz)(t,a).link;if(!l)throw new Error(`DocSidebarNavbarItem: Sidebar with ID "${t}" doesn't have anything to be linked to.`);return r.createElement(re,(0,b.Z)({exact:!0},o,{isActive:()=>(null==i?void 0:i.sidebar)===t,label:n??l.label,to:l.path}))},docsVersion:function(e){let{label:t,to:n,docsPluginId:a,...o}=e;const i=(0,ve.lO)(a)[0],l=t??i.label,s=n??(e=>e.docs.find((t=>t.id===e.mainDocId)))(i).path;return r.createElement(re,(0,b.Z)({},o,{label:l,to:s}))},docsVersionDropdown:function(e){let{mobile:t,docsPluginId:n,dropdownActiveClassDisabled:a,dropdownItemsBefore:o,dropdownItemsAfter:i,...l}=e;const s=(0,be.Iw)(n),c=(0,be.gB)(n),{savePreferredVersionName:d}=(0,ye.J)(n),f=[...o,...c.map((e=>{const t=s.alternateDocVersions[e.name]??we(e);return{label:e.label,to:t.path,isActive:()=>e===s.activeVersion,onClick:()=>d(e.name)}})),...i],p=(0,ve.lO)(n)[0],m=t&&f.length>1?(0,u.I)({id:"theme.navbar.mobileVersionsDropdown.label",message:"Versions",description:"The label for the navbar versions dropdown on mobile view"}):p.label,g=t&&f.length>1?void 0:we(p).path;return f.length<=1?r.createElement(re,(0,b.Z)({},l,{mobile:t,label:m,to:g,isActive:a?()=>!1:void 0})):r.createElement(ce,(0,b.Z)({},l,{mobile:t,label:m,to:g,items:f,isActive:a?()=>!1:void 0}))}};function Ee(e){let{type:t,...n}=e;const a=function(e,t){return e&&"default"!==e?e:"items"in t?"dropdown":"default"}(t,n),o=ke[a];if(!o)throw new Error(`No NavbarItem component found for type "${t}".`);return r.createElement(o,n)}function Se(){const e=(0,A.e)(),t=(0,g.L)().navbar.items;return r.createElement("ul",{className:"menu__list"},t.map(((t,n)=>r.createElement(Ee,(0,b.Z)({mobile:!0},t,{onClick:()=>e.toggle(),key:n})))))}function xe(e){return r.createElement("button",(0,b.Z)({},e,{type:"button",className:"clean-btn navbar-sidebar__back"}),r.createElement(u.Z,{id:"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel",description:"The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)"},"\u2190 Back to main menu"))}function _e(){const e=0===(0,g.L)().navbar.items.length,t=I();return r.createElement(r.Fragment,null,!e&&r.createElement(xe,{onClick:()=>t.hide()}),t.content)}function Ce(){const e=(0,A.e)();var t;return void 0===(t=e.shown)&&(t=!0),(0,r.useEffect)((()=>(document.body.style.overflow=t?"hidden":"visible",()=>{document.body.style.overflow="visible"})),[t]),e.shouldRender?r.createElement(M,{header:r.createElement(W,null),primaryMenu:r.createElement(Se,null),secondaryMenu:r.createElement(_e,null)}):null}const Te="navbarHideable_m1mJ",Ae="navbarHidden_jGov";function Le(e){return r.createElement("div",(0,b.Z)({role:"presentation"},e,{className:(0,a.Z)("navbar-sidebar__backdrop",e.className)}))}function Re(e){let{children:t}=e;const{navbar:{hideOnScroll:n,style:o}}=(0,g.L)(),i=(0,A.e)(),{navbarRef:l,isNavbarVisible:s}=function(e){const[t,n]=(0,r.useState)(e),a=(0,r.useRef)(!1),o=(0,r.useRef)(0),i=(0,r.useCallback)((e=>{null!==e&&(o.current=e.getBoundingClientRect().height)}),[]);return(0,L.RF)(((t,r)=>{let{scrollY:i}=t;if(!e)return;if(i=l?n(!1):i+u{if(e)return t.location.hash?(a.current=!0,void n(!1)):void n(!0)})),{navbarRef:i,isNavbarVisible:t}}(n);return r.createElement("nav",{ref:l,className:(0,a.Z)("navbar","navbar--fixed-top",n&&[Te,!s&&Ae],{"navbar--dark":"dark"===o,"navbar--primary":"primary"===o,"navbar-sidebar--show":i.shown})},t,r.createElement(Le,{onClick:i.toggle}),r.createElement(Ce,null))}function Pe(e){let{width:t=30,height:n=30,className:a,...o}=e;return r.createElement("svg",(0,b.Z)({className:a,width:t,height:n,viewBox:"0 0 30 30","aria-hidden":"true"},o),r.createElement("path",{stroke:"currentColor",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"2",d:"M4 7h22M4 15h22M4 23h22"}))}function Ne(){const e=(0,A.e)();return r.createElement("button",{onClick:e.toggle,onKeyDown:e.toggle,"aria-label":"Navigation bar toggle",className:"navbar__toggle clean-btn",type:"button",tabIndex:0},r.createElement(Pe,null))}const Oe="colorModeToggle_DEke";function De(e){let{items:t}=e;return r.createElement(r.Fragment,null,t.map(((e,t)=>r.createElement(Ee,(0,b.Z)({},e,{key:t})))))}function Ie(e){let{left:t,right:n}=e;return r.createElement("div",{className:"navbar__inner"},r.createElement("div",{className:"navbar__items"},t),r.createElement("div",{className:"navbar__items navbar__items--right"},n))}function Me(){const e=(0,A.e)(),t=(0,g.L)().navbar.items,[n,a]=function(e){function t(e){return"left"===(e.position??"right")}return[e.filter(t),e.filter((e=>!t(e)))]}(t),o=t.find((e=>"search"===e.type));return r.createElement(Ie,{left:r.createElement(r.Fragment,null,!e.disabled&&r.createElement(Ne,null),r.createElement(Z,null),r.createElement(De,{items:n})),right:r.createElement(r.Fragment,null,r.createElement(De,{items:a}),r.createElement(G,{className:Oe}),!o&&r.createElement(he,null,r.createElement(me,null)))})}function je(){return r.createElement(Re,null,r.createElement(Me,null))}function Fe(e){let{item:t}=e;const{to:n,href:a,label:o,prependBaseUrlToHref:i,...l}=t,s=(0,K.Z)(n),u=(0,K.Z)(a,{forcePrependBaseUrl:!0});return r.createElement(Y.Z,(0,b.Z)({className:"footer__link-item"},a?{href:i?u:a}:{to:s},l),o,a&&!(0,Q.Z)(a)&&r.createElement(J.Z,null))}function Be(e){let{item:t}=e;return t.html?r.createElement("li",{className:"footer__item",dangerouslySetInnerHTML:{__html:t.html}}):r.createElement("li",{key:t.href??t.to,className:"footer__item"},r.createElement(Fe,{item:t}))}function ze(e){let{column:t}=e;return r.createElement("div",{className:"col footer__col"},r.createElement("div",{className:"footer__title"},t.title),r.createElement("ul",{className:"footer__items clean-list"},t.items.map(((e,t)=>r.createElement(Be,{key:t,item:e})))))}function Ue(e){let{columns:t}=e;return r.createElement("div",{className:"row footer__links"},t.map(((e,t)=>r.createElement(ze,{key:t,column:e}))))}function $e(){return r.createElement("span",{className:"footer__link-separator"},"\xb7")}function qe(e){let{item:t}=e;return t.html?r.createElement("span",{className:"footer__link-item",dangerouslySetInnerHTML:{__html:t.html}}):r.createElement(Fe,{item:t})}function Ge(e){let{links:t}=e;return r.createElement("div",{className:"footer__links text--center"},r.createElement("div",{className:"footer__links"},t.map(((e,n)=>r.createElement(r.Fragment,{key:n},r.createElement(qe,{item:e}),t.length!==n+1&&r.createElement($e,null))))))}function He(e){let{links:t}=e;return function(e){return"title"in e[0]}(t)?r.createElement(Ue,{columns:t}):r.createElement(Ge,{links:t})}var Ze=n(941);const Ve="footerLogoLink_BH7S";function We(e){let{logo:t}=e;const{withBaseUrl:n}=(0,K.C)(),o={light:n(t.src),dark:n(t.srcDark??t.src)};return r.createElement(Ze.Z,{className:(0,a.Z)("footer__logo",t.className),alt:t.alt,sources:o,width:t.width,height:t.height,style:t.style})}function Ye(e){let{logo:t}=e;return t.href?r.createElement(Y.Z,{href:t.href,className:Ve,target:t.target},r.createElement(We,{logo:t})):r.createElement(We,{logo:t})}function Ke(e){let{copyright:t}=e;return r.createElement("div",{className:"footer__copyright",dangerouslySetInnerHTML:{__html:t}})}function Qe(e){let{style:t,links:n,logo:o,copyright:i}=e;return r.createElement("footer",{className:(0,a.Z)("footer",{"footer--dark":"dark"===t})},r.createElement("div",{className:"container container-fluid"},n,(o||i)&&r.createElement("div",{className:"footer__bottom text--center"},o&&r.createElement("div",{className:"margin-bottom--sm"},o),i)))}function Xe(){const{footer:e}=(0,g.L)();if(!e)return null;const{copyright:t,links:n,logo:a,style:o}=e;return r.createElement(Qe,{style:o,links:n&&n.length>0&&r.createElement(He,{links:n}),logo:a&&r.createElement(Ye,{logo:a}),copyright:t&&r.createElement(Ke,{copyright:t})})}const Je=r.memo(Xe);var et=n(12);const tt="docusaurus.tab.",nt=r.createContext(void 0);const rt=(0,R.Qc)([j.S,h.pl,function(e){let{children:t}=e;const n=function(){const[e,t]=(0,r.useState)({}),n=(0,r.useCallback)(((e,t)=>{(0,et.W)(`docusaurus.tab.${e}`).set(t)}),[]);(0,r.useEffect)((()=>{try{const e={};(0,et._)().forEach((t=>{if(t.startsWith(tt)){const n=t.substring(tt.length);e[n]=(0,et.W)(t).get()}})),t(e)}catch(e){console.error(e)}}),[]);const a=(0,r.useCallback)(((e,r)=>{t((t=>({...t,[e]:r}))),n(e,r)}),[n]);return(0,r.useMemo)((()=>({tabGroupChoices:e,setTabGroupChoices:a})),[e,a])}();return r.createElement(nt.Provider,{value:n},t)},L.OC,ye.L5,i.VC,function(e){let{children:t}=e;return r.createElement(P.n2,null,r.createElement(A.M,null,r.createElement(O,null,t)))}]);function at(e){let{children:t}=e;return r.createElement(rt,null,t)}function ot(e){let{error:t,tryAgain:n}=e;return r.createElement("main",{className:"container margin-vert--xl"},r.createElement("div",{className:"row"},r.createElement("div",{className:"col col--6 col--offset-3"},r.createElement("h1",{className:"hero__title"},r.createElement(u.Z,{id:"theme.ErrorPageContent.title",description:"The title of the fallback page when the page crashed"},"This page crashed.")),r.createElement("p",null,t.message),r.createElement("div",null,r.createElement("button",{type:"button",onClick:n},r.createElement(u.Z,{id:"theme.ErrorPageContent.tryAgain",description:"The label of the button to try again when the page crashed"},"Try again"))))))}const it="mainWrapper_z2l0";function lt(e){const{children:t,noFooter:n,wrapperClassName:u,title:c,description:d}=e;return(0,s.t)(),r.createElement(at,null,r.createElement(i.d,{title:c,description:d}),r.createElement(m,null),r.createElement(T,null),r.createElement(je,null),r.createElement("div",{className:(0,a.Z)(l.k.wrapper.main,it,u)},r.createElement(o.Z,{fallback:e=>r.createElement(ot,e)},t)),!n&&r.createElement(Je,null))}},1327:(e,t,n)=>{"use strict";n.d(t,{Z:()=>d});var r=n(7462),a=n(7294),o=n(9960),i=n(4996),l=n(2263),s=n(6668),u=n(941);function c(e){let{logo:t,alt:n,imageClassName:r}=e;const o={light:(0,i.Z)(t.src),dark:(0,i.Z)(t.srcDark||t.src)},l=a.createElement(u.Z,{className:t.className,sources:o,height:t.height,width:t.width,alt:n,style:t.style});return r?a.createElement("div",{className:r},l):l}function d(e){const{siteConfig:{title:t}}=(0,l.Z)(),{navbar:{title:n,logo:u}}=(0,s.L)(),{imageClassName:d,titleClassName:f,...p}=e,m=(0,i.Z)((null==u?void 0:u.href)||"/"),g=n?"":t,h=(null==u?void 0:u.alt)??g;return a.createElement(o.Z,(0,r.Z)({to:m},p,(null==u?void 0:u.target)&&{target:u.target}),u&&a.createElement(c,{logo:u,alt:h,imageClassName:d}),null!=n&&a.createElement("b",{className:f},n))}},197:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});var r=n(7294),a=n(5742);function o(e){let{locale:t,version:n,tag:o}=e;const i=t;return r.createElement(a.Z,null,t&&r.createElement("meta",{name:"docusaurus_locale",content:t}),n&&r.createElement("meta",{name:"docusaurus_version",content:n}),o&&r.createElement("meta",{name:"docusaurus_tag",content:o}),i&&r.createElement("meta",{name:"docsearch:language",content:i}),n&&r.createElement("meta",{name:"docsearch:version",content:n}),o&&r.createElement("meta",{name:"docsearch:docusaurus_tag",content:o}))}},941:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});var r=n(7462),a=n(7294),o=n(6010),i=n(2389),l=n(2949);const s={themedImage:"themedImage_ToTc","themedImage--light":"themedImage--light_HNdA","themedImage--dark":"themedImage--dark_i4oU"};function u(e){const t=(0,i.Z)(),{colorMode:n}=(0,l.I)(),{sources:u,className:c,alt:d,...f}=e,p=t?"dark"===n?["dark"]:["light"]:["light","dark"];return a.createElement(a.Fragment,null,p.map((e=>a.createElement("img",(0,r.Z)({key:e,src:u[e],alt:d,className:(0,o.Z)(s.themedImage,s[`themedImage--${e}`],c)},f)))))}},6043:(e,t,n)=>{"use strict";n.d(t,{u:()=>i,z:()=>m});var r=n(7462),a=n(7294),o=n(412);function i(e){let{initialState:t}=e;const[n,r]=(0,a.useState)(t??!1),o=(0,a.useCallback)((()=>{r((e=>!e))}),[]);return{collapsed:n,setCollapsed:r,toggleCollapsed:o}}const l={display:"none",overflow:"hidden",height:"0px"},s={display:"block",overflow:"visible",height:"auto"};function u(e,t){const n=t?l:s;e.style.display=n.display,e.style.overflow=n.overflow,e.style.height=n.height}function c(e){let{collapsibleRef:t,collapsed:n,animation:r}=e;const o=(0,a.useRef)(!1);(0,a.useEffect)((()=>{const e=t.current;function a(){const t=e.scrollHeight,n=(null==r?void 0:r.duration)??function(e){const t=e/36;return Math.round(10*(4+15*t**.25+t/5))}(t);return{transition:`height ${n}ms ${(null==r?void 0:r.easing)??"ease-in-out"}`,height:`${t}px`}}function i(){const t=a();e.style.transition=t.transition,e.style.height=t.height}if(!o.current)return u(e,n),void(o.current=!0);return e.style.willChange="height",function(){const t=requestAnimationFrame((()=>{n?(i(),requestAnimationFrame((()=>{e.style.height=l.height,e.style.overflow=l.overflow}))):(e.style.display="block",requestAnimationFrame((()=>{i()})))}));return()=>cancelAnimationFrame(t)}()}),[t,n,r])}function d(e){if(!o.Z.canUseDOM)return e?l:s}function f(e){let{as:t="div",collapsed:n,children:r,animation:o,onCollapseTransitionEnd:i,className:l,disableSSRStyle:s}=e;const f=(0,a.useRef)(null);return c({collapsibleRef:f,collapsed:n,animation:o}),a.createElement(t,{ref:f,style:s?void 0:d(n),onTransitionEnd:e=>{"height"===e.propertyName&&(u(f.current,n),null==i||i(n))},className:l},r)}function p(e){let{collapsed:t,...n}=e;const[o,i]=(0,a.useState)(!t),[l,s]=(0,a.useState)(t);return(0,a.useLayoutEffect)((()=>{t||i(!0)}),[t]),(0,a.useLayoutEffect)((()=>{o&&s(t)}),[o,t]),o?a.createElement(f,(0,r.Z)({},n,{collapsed:l})):null}function m(e){let{lazy:t,...n}=e;const r=t?p:f;return a.createElement(r,n)}},9689:(e,t,n)=>{"use strict";n.d(t,{nT:()=>m,pl:()=>p});var r=n(7294),a=n(2389),o=n(12),i=n(902),l=n(6668);const s=(0,o.W)("docusaurus.announcement.dismiss"),u=(0,o.W)("docusaurus.announcement.id"),c=()=>"true"===s.get(),d=e=>s.set(String(e)),f=r.createContext(null);function p(e){let{children:t}=e;const n=function(){const{announcementBar:e}=(0,l.L)(),t=(0,a.Z)(),[n,o]=(0,r.useState)((()=>!!t&&c()));(0,r.useEffect)((()=>{o(c())}),[]);const i=(0,r.useCallback)((()=>{d(!0),o(!0)}),[]);return(0,r.useEffect)((()=>{if(!e)return;const{id:t}=e;let n=u.get();"annoucement-bar"===n&&(n="announcement-bar");const r=t!==n;u.set(t),r&&d(!1),!r&&c()||o(!1)}),[e]),(0,r.useMemo)((()=>({isActive:!!e&&!n,close:i})),[e,n,i])}();return r.createElement(f.Provider,{value:n},t)}function m(){const e=(0,r.useContext)(f);if(!e)throw new i.i6("AnnouncementBarProvider");return e}},2949:(e,t,n)=>{"use strict";n.d(t,{I:()=>h,S:()=>g});var r=n(7294),a=n(412),o=n(902),i=n(12),l=n(6668);const s=r.createContext(void 0),u="theme",c=(0,i.W)(u),d="light",f="dark",p=e=>e===f?f:d;function m(){const{colorMode:{defaultMode:e,disableSwitch:t,respectPrefersColorScheme:n}}=(0,l.L)(),[o,i]=(0,r.useState)((e=>a.Z.canUseDOM?p(document.documentElement.getAttribute("data-theme")):p(e))(e));(0,r.useEffect)((()=>{t&&c.del()}),[t]);const s=(0,r.useCallback)((function(t,r){void 0===r&&(r={});const{persist:a=!0}=r;t?(i(t),a&&(e=>{c.set(p(e))})(t)):(i(n?window.matchMedia("(prefers-color-scheme: dark)").matches?f:d:e),c.del())}),[n,e]);(0,r.useEffect)((()=>{document.documentElement.setAttribute("data-theme",p(o))}),[o]),(0,r.useEffect)((()=>{if(t)return;const e=e=>{if(e.key!==u)return;const t=c.get();null!==t&&s(p(t))};return window.addEventListener("storage",e),()=>window.removeEventListener("storage",e)}),[t,s]);const m=(0,r.useRef)(!1);return(0,r.useEffect)((()=>{if(t&&!n)return;const e=window.matchMedia("(prefers-color-scheme: dark)"),r=()=>{window.matchMedia("print").matches||m.current?m.current=window.matchMedia("print").matches:s(null)};return e.addListener(r),()=>e.removeListener(r)}),[s,t,n]),(0,r.useMemo)((()=>({colorMode:o,setColorMode:s,get isDarkTheme(){return o===f},setLightTheme(){s(d)},setDarkTheme(){s(f)}})),[o,s])}function g(e){let{children:t}=e;const n=m();return r.createElement(s.Provider,{value:n},t)}function h(){const e=(0,r.useContext)(s);if(null==e)throw new o.i6("ColorModeProvider","Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.");return e}},373:(e,t,n)=>{"use strict";n.d(t,{J:()=>y,L5:()=>b});var r=n(7294),a=n(143),o=n(9935),i=n(6668),l=n(2802),s=n(902),u=n(12);const c=e=>`docs-preferred-version-${e}`,d=(e,t,n)=>{(0,u.W)(c(e),{persistence:t}).set(n)},f=(e,t)=>(0,u.W)(c(e),{persistence:t}).get(),p=(e,t)=>{(0,u.W)(c(e),{persistence:t}).del()};const m=r.createContext(null);function g(){const e=(0,a._r)(),t=(0,i.L)().docs.versionPersistence,n=(0,r.useMemo)((()=>Object.keys(e)),[e]),[o,l]=(0,r.useState)((()=>(e=>Object.fromEntries(e.map((e=>[e,{preferredVersionName:null}]))))(n)));(0,r.useEffect)((()=>{l(function(e){let{pluginIds:t,versionPersistence:n,allDocsData:r}=e;function a(e){const t=f(e,n);return r[e].versions.some((e=>e.name===t))?{preferredVersionName:t}:(p(e,n),{preferredVersionName:null})}return Object.fromEntries(t.map((e=>[e,a(e)])))}({allDocsData:e,versionPersistence:t,pluginIds:n}))}),[e,t,n]);return[o,(0,r.useMemo)((()=>({savePreferredVersion:function(e,n){d(e,t,n),l((t=>({...t,[e]:{preferredVersionName:n}})))}})),[t])]}function h(e){let{children:t}=e;const n=g();return r.createElement(m.Provider,{value:n},t)}function b(e){let{children:t}=e;return l.cE?r.createElement(h,null,t):r.createElement(r.Fragment,null,t)}function v(){const e=(0,r.useContext)(m);if(!e)throw new s.i6("DocsPreferredVersionContextProvider");return e}function y(e){void 0===e&&(e=o.m);const t=(0,a.zh)(e),[n,i]=v(),{preferredVersionName:l}=n[e];return{preferredVersion:t.versions.find((e=>e.name===l))??null,savePreferredVersionName:(0,r.useCallback)((t=>{i.savePreferredVersion(e,t)}),[i,e])}}},1116:(e,t,n)=>{"use strict";n.d(t,{V:()=>s,b:()=>l});var r=n(7294),a=n(902);const o=Symbol("EmptyContext"),i=r.createContext(o);function l(e){let{children:t,name:n,items:a}=e;const o=(0,r.useMemo)((()=>n&&a?{name:n,items:a}:null),[n,a]);return r.createElement(i.Provider,{value:o},t)}function s(){const e=(0,r.useContext)(i);if(e===o)throw new a.i6("DocsSidebarProvider");return e}},2961:(e,t,n)=>{"use strict";n.d(t,{M:()=>f,e:()=>p});var r=n(7294),a=n(3102),o=n(7524),i=n(6775),l=n(902);function s(e){!function(e){const t=(0,i.k6)(),n=(0,l.zX)(e);(0,r.useEffect)((()=>t.block(((e,t)=>n(e,t)))),[t,n])}(((t,n)=>{if("POP"===n)return e(t,n)}))}var u=n(6668);const c=r.createContext(void 0);function d(){const e=function(){const e=(0,a.HY)(),{items:t}=(0,u.L)().navbar;return 0===t.length&&!e.component}(),t=(0,o.i)(),n=!e&&"mobile"===t,[i,l]=(0,r.useState)(!1);s((()=>{if(i)return l(!1),!1}));const c=(0,r.useCallback)((()=>{l((e=>!e))}),[]);return(0,r.useEffect)((()=>{"desktop"===t&&l(!1)}),[t]),(0,r.useMemo)((()=>({disabled:e,shouldRender:n,toggle:c,shown:i})),[e,n,c,i])}function f(e){let{children:t}=e;const n=d();return r.createElement(c.Provider,{value:n},t)}function p(){const e=r.useContext(c);if(void 0===e)throw new l.i6("NavbarMobileSidebarProvider");return e}},3102:(e,t,n)=>{"use strict";n.d(t,{HY:()=>l,Zo:()=>s,n2:()=>i});var r=n(7294),a=n(902);const o=r.createContext(null);function i(e){let{children:t}=e;const n=(0,r.useState)({component:null,props:null});return r.createElement(o.Provider,{value:n},t)}function l(){const e=(0,r.useContext)(o);if(!e)throw new a.i6("NavbarSecondaryMenuContentProvider");return e[0]}function s(e){let{component:t,props:n}=e;const i=(0,r.useContext)(o);if(!i)throw new a.i6("NavbarSecondaryMenuContentProvider");const[,l]=i,s=(0,a.Ql)(n);return(0,r.useEffect)((()=>{l({component:t,props:s})}),[l,t,s]),(0,r.useEffect)((()=>()=>l({component:null,props:null})),[l]),null}},9727:(e,t,n)=>{"use strict";n.d(t,{h:()=>a,t:()=>o});var r=n(7294);const a="navigation-with-keyboard";function o(){(0,r.useEffect)((()=>{function e(e){"keydown"===e.type&&"Tab"===e.key&&document.body.classList.add(a),"mousedown"===e.type&&document.body.classList.remove(a)}return document.addEventListener("keydown",e),document.addEventListener("mousedown",e),()=>{document.body.classList.remove(a),document.removeEventListener("keydown",e),document.removeEventListener("mousedown",e)}}),[])}},7524:(e,t,n)=>{"use strict";n.d(t,{i:()=>u});var r=n(7294),a=n(412);const o="desktop",i="mobile",l="ssr";function s(){return a.Z.canUseDOM?window.innerWidth>996?o:i:l}function u(){const[e,t]=(0,r.useState)((()=>s()));return(0,r.useEffect)((()=>{function e(){t(s())}return window.addEventListener("resize",e),()=>{window.removeEventListener("resize",e),clearTimeout(undefined)}}),[]),e}},5281:(e,t,n)=>{"use strict";n.d(t,{k:()=>r});const r={page:{blogListPage:"blog-list-page",blogPostPage:"blog-post-page",blogTagsListPage:"blog-tags-list-page",blogTagPostListPage:"blog-tags-post-list-page",docsDocPage:"docs-doc-page",docsTagsListPage:"docs-tags-list-page",docsTagDocListPage:"docs-tags-doc-list-page",mdxPage:"mdx-page"},wrapper:{main:"main-wrapper",blogPages:"blog-wrapper",docsPages:"docs-wrapper",mdxPages:"mdx-wrapper"},common:{editThisPage:"theme-edit-this-page",lastUpdated:"theme-last-updated",backToTopButton:"theme-back-to-top-button",codeBlock:"theme-code-block",admonition:"theme-admonition",admonitionType:e=>`theme-admonition-${e}`},layout:{},docs:{docVersionBanner:"theme-doc-version-banner",docVersionBadge:"theme-doc-version-badge",docBreadcrumbs:"theme-doc-breadcrumbs",docMarkdown:"theme-doc-markdown",docTocMobile:"theme-doc-toc-mobile",docTocDesktop:"theme-doc-toc-desktop",docFooter:"theme-doc-footer",docFooterTagsRow:"theme-doc-footer-tags-row",docFooterEditMetaRow:"theme-doc-footer-edit-meta-row",docSidebarContainer:"theme-doc-sidebar-container",docSidebarMenu:"theme-doc-sidebar-menu",docSidebarItemCategory:"theme-doc-sidebar-item-category",docSidebarItemLink:"theme-doc-sidebar-item-link",docSidebarItemCategoryLevel:e=>`theme-doc-sidebar-item-category-level-${e}`,docSidebarItemLinkLevel:e=>`theme-doc-sidebar-item-link-level-${e}`},blog:{}}},2802:(e,t,n)=>{"use strict";n.d(t,{Wl:()=>f,_F:()=>m,cE:()=>d,hI:()=>w,lO:()=>b,vY:()=>y,oz:()=>v,s1:()=>h});var r=n(7294),a=n(6775),o=n(8790),i=n(143),l=n(373),s=n(1116);function u(e){return Array.from(new Set(e))}var c=n(8596);const d=!!i._r;function f(e){if(e.href)return e.href;for(const t of e.items){if("link"===t.type)return t.href;if("category"===t.type){const e=f(t);if(e)return e}}}const p=(e,t)=>void 0!==e&&(0,c.Mg)(e,t);function m(e,t){return"link"===e.type?p(e.href,t):"category"===e.type&&(p(e.href,t)||((e,t)=>e.some((e=>m(e,t))))(e.items,t))}function g(e){let{sidebarItems:t,pathname:n,onlyCategories:r=!1}=e;const a=[];return function e(t){for(const o of t)if("category"===o.type&&((0,c.Mg)(o.href,n)||e(o.items))||"link"===o.type&&(0,c.Mg)(o.href,n)){return r&&"category"!==o.type||a.unshift(o),!0}return!1}(t),a}function h(){var e;const t=(0,s.V)(),{pathname:n}=(0,a.TH)();return!1!==(null==(e=(0,i.gA)())?void 0:e.pluginData.breadcrumbs)&&t?g({sidebarItems:t.items,pathname:n}):null}function b(e){const{activeVersion:t}=(0,i.Iw)(e),{preferredVersion:n}=(0,l.J)(e),a=(0,i.yW)(e);return(0,r.useMemo)((()=>u([t,n,a].filter(Boolean))),[t,n,a])}function v(e,t){const n=b(t);return(0,r.useMemo)((()=>{const t=n.flatMap((e=>e.sidebars?Object.entries(e.sidebars):[])),r=t.find((t=>t[0]===e));if(!r)throw new Error(`Can't find any sidebar with id "${e}" in version${n.length>1?"s":""} ${n.map((e=>e.name)).join(", ")}".\n Available sidebar ids are:\n - ${Object.keys(t).join("\n- ")}`);return r[1]}),[e,n])}function y(e,t){const n=b(t);return(0,r.useMemo)((()=>{const t=n.flatMap((e=>e.docs)),r=t.find((t=>t.id===e));if(!r){if(n.flatMap((e=>e.draftIds)).includes(e))return null;throw new Error(`DocNavbarItem: couldn't find any doc with id "${e}" in version${n.length>1?"s":""} ${n.map((e=>e.name)).join(", ")}".\nAvailable doc ids are:\n- ${u(t.map((e=>e.id))).join("\n- ")}`)}return r}),[e,n])}function w(e){let{route:t,versionMetadata:n}=e;const r=(0,a.TH)(),i=t.routes,l=i.find((e=>(0,a.LX)(r.pathname,e)));if(!l)return null;const s=l.sidebar,u=s?n.docsSidebars[s]:void 0;return{docElement:(0,o.H)(i),sidebarName:s,sidebarItems:u}}},1944:(e,t,n)=>{"use strict";n.d(t,{FG:()=>f,d:()=>c,VC:()=>p});var r=n(7294),a=n(6010),o=n(5742),i=n(226);function l(){const e=r.useContext(i._);if(!e)throw new Error("Unexpected: no Docusaurus route context found");return e}var s=n(4996),u=n(2263);function c(e){let{title:t,description:n,keywords:a,image:i,children:l}=e;const c=function(e){const{siteConfig:t}=(0,u.Z)(),{title:n,titleDelimiter:r}=t;return null!=e&&e.trim().length?`${e.trim()} ${r} ${n}`:n}(t),{withBaseUrl:d}=(0,s.C)(),f=i?d(i,{absolute:!0}):void 0;return r.createElement(o.Z,null,t&&r.createElement("title",null,c),t&&r.createElement("meta",{property:"og:title",content:c}),n&&r.createElement("meta",{name:"description",content:n}),n&&r.createElement("meta",{property:"og:description",content:n}),a&&r.createElement("meta",{name:"keywords",content:Array.isArray(a)?a.join(","):a}),f&&r.createElement("meta",{property:"og:image",content:f}),f&&r.createElement("meta",{name:"twitter:image",content:f}),l)}const d=r.createContext(void 0);function f(e){let{className:t,children:n}=e;const i=r.useContext(d),l=(0,a.Z)(i,t);return r.createElement(d.Provider,{value:l},r.createElement(o.Z,null,r.createElement("html",{className:l})),n)}function p(e){let{children:t}=e;const n=l(),o=`plugin-${n.plugin.name.replace(/docusaurus-(?:plugin|theme)-(?:content-)?/gi,"")}`;const i=`plugin-id-${n.plugin.id}`;return r.createElement(f,{className:(0,a.Z)(o,i)},t)}},902:(e,t,n)=>{"use strict";n.d(t,{D9:()=>i,Qc:()=>u,Ql:()=>s,i6:()=>l,zX:()=>o});var r=n(7294);const a=n(412).Z.canUseDOM?r.useLayoutEffect:r.useEffect;function o(e){const t=(0,r.useRef)(e);return a((()=>{t.current=e}),[e]),(0,r.useCallback)((function(){return t.current(...arguments)}),[])}function i(e){const t=(0,r.useRef)();return a((()=>{t.current=e})),t.current}class l extends Error{constructor(e,t){var n,r,a;super(),this.name="ReactContextError",this.message=`Hook ${(null==(n=this.stack)||null==(r=n.split("\n")[1])||null==(a=r.match(/at (?:\w+\.)?(?\w+)/))?void 0:a.groups.name)??""} is called outside the <${e}>. ${t??""}`}}function s(e){const t=Object.entries(e);return t.sort(((e,t)=>e[0].localeCompare(t[0]))),(0,r.useMemo)((()=>e),t.flat())}function u(e){return t=>{let{children:n}=t;return r.createElement(r.Fragment,null,e.reduceRight(((e,t)=>r.createElement(t,null,e)),n))}}},8596:(e,t,n)=>{"use strict";n.d(t,{Mg:()=>i,Ns:()=>l});var r=n(7294),a=n(723),o=n(2263);function i(e,t){const n=e=>{var t;return null==(t=!e||e.endsWith("/")?e:`${e}/`)?void 0:t.toLowerCase()};return n(e)===n(t)}function l(){const{baseUrl:e}=(0,o.Z)().siteConfig;return(0,r.useMemo)((()=>function(e){let{baseUrl:t,routes:n}=e;function r(e){return e.path===t&&!0===e.exact}function a(e){return e.path===t&&!e.exact}return function e(t){if(0===t.length)return;return t.find(r)||e(t.filter(a).flatMap((e=>e.routes??[])))}(n)}({routes:a.Z,baseUrl:e})),[e])}},2466:(e,t,n)=>{"use strict";n.d(t,{Ct:()=>f,OC:()=>s,RF:()=>d});var r=n(7294),a=n(412),o=n(2389),i=n(902);const l=r.createContext(void 0);function s(e){let{children:t}=e;const n=function(){const e=(0,r.useRef)(!0);return(0,r.useMemo)((()=>({scrollEventsEnabledRef:e,enableScrollEvents:()=>{e.current=!0},disableScrollEvents:()=>{e.current=!1}})),[])}();return r.createElement(l.Provider,{value:n},t)}function u(){const e=(0,r.useContext)(l);if(null==e)throw new i.i6("ScrollControllerProvider");return e}const c=()=>a.Z.canUseDOM?{scrollX:window.pageXOffset,scrollY:window.pageYOffset}:null;function d(e,t){void 0===t&&(t=[]);const{scrollEventsEnabledRef:n}=u(),a=(0,r.useRef)(c()),o=(0,i.zX)(e);(0,r.useEffect)((()=>{const e=()=>{if(!n.current)return;const e=c();o(e,a.current),a.current=e},t={passive:!0};return e(),window.addEventListener("scroll",e,t),()=>window.removeEventListener("scroll",e,t)}),[o,n,...t])}function f(){const e=(0,r.useRef)(null),t=(0,o.Z)()&&"smooth"===getComputedStyle(document.documentElement).scrollBehavior;return{startScroll:n=>{e.current=t?function(e){return window.scrollTo({top:e,behavior:"smooth"}),()=>{}}(n):function(e){let t=null;const n=document.documentElement.scrollTop>e;return function r(){const a=document.documentElement.scrollTop;(n&&a>e||!n&&at&&cancelAnimationFrame(t)}(n)},cancelScroll:()=>null==e.current?void 0:e.current()}}},3320:(e,t,n)=>{"use strict";n.d(t,{HX:()=>r,os:()=>a});n(2263);const r="default";function a(e,t){return`docs-${e}-${t}`}},12:(e,t,n)=>{"use strict";n.d(t,{W:()=>l,_:()=>s});const r="localStorage";function a(e){if(void 0===e&&(e=r),"undefined"==typeof window)throw new Error("Browser storage is not available on Node.js/Docusaurus SSR process.");if("none"===e)return null;try{return window[e]}catch(n){return t=n,o||(console.warn("Docusaurus browser storage is not available.\nPossible reasons: running Docusaurus in an iframe, in an incognito browser session, or using too strict browser privacy settings.",t),o=!0),null}var t}let o=!1;const i={get:()=>null,set:()=>{},del:()=>{}};function l(e,t){if("undefined"==typeof window)return function(e){function t(){throw new Error(`Illegal storage API usage for storage key "${e}".\nDocusaurus storage APIs are not supposed to be called on the server-rendering process.\nPlease only call storage APIs in effects and event handlers.`)}return{get:t,set:t,del:t}}(e);const n=a(null==t?void 0:t.persistence);return null===n?i:{get:()=>{try{return n.getItem(e)}catch(t){return console.error(`Docusaurus storage error, can't get key=${e}`,t),null}},set:t=>{try{n.setItem(e,t)}catch(r){console.error(`Docusaurus storage error, can't set ${e}=${t}`,r)}},del:()=>{try{n.removeItem(e)}catch(t){console.error(`Docusaurus storage error, can't delete key=${e}`,t)}}}}function s(e){void 0===e&&(e=r);const t=a(e);if(!t)return[];const n=[];for(let r=0;r{"use strict";n.d(t,{l:()=>o});var r=n(2263),a=n(6775);function o(){const{siteConfig:{baseUrl:e,url:t},i18n:{defaultLocale:n,currentLocale:o}}=(0,r.Z)(),{pathname:i}=(0,a.TH)(),l=o===n?e:e.replace(`/${o}/`,"/"),s=i.replace(e,"");return{createUrl:function(e){let{locale:r,fullyQualified:a}=e;return`${a?t:""}${function(e){return e===n?`${l}`:`${l}${e}/`}(r)}${s}`}}}},5936:(e,t,n)=>{"use strict";n.d(t,{S:()=>i});var r=n(7294),a=n(6775),o=n(902);function i(e){const t=(0,a.TH)(),n=(0,o.D9)(t),i=(0,o.zX)(e);(0,r.useEffect)((()=>{n&&t!==n&&i({location:t,previousLocation:n})}),[i,t,n])}},6668:(e,t,n)=>{"use strict";n.d(t,{L:()=>a});var r=n(2263);function a(){return(0,r.Z)().siteConfig.themeConfig}},8802:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){const{trailingSlash:n,baseUrl:r}=t;if(e.startsWith("#"))return e;if(void 0===n)return e;const[a]=e.split(/[#?]/),o="/"===a||a===r?a:(i=a,n?function(e){return e.endsWith("/")?e:`${e}/`}(i):function(e){return e.endsWith("/")?e.slice(0,-1):e}(i));var i;return e.replace(a,o)}},8780:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.applyTrailingSlash=t.blogPostContainerID=void 0,t.blogPostContainerID="post-content";var a=n(8802);Object.defineProperty(t,"applyTrailingSlash",{enumerable:!0,get:function(){return r(a).default}})},6010:(e,t,n)=>{"use strict";function r(e){var t,n,a="";if("string"==typeof e||"number"==typeof e)a+=e;else if("object"==typeof e)if(Array.isArray(e))for(t=0;ta});const a=function(){for(var e,t,n=0,a="";n{"use strict";n.d(t,{lX:()=>w,q_:()=>C,ob:()=>p,PP:()=>A,Ep:()=>f});var r=n(7462);function a(e){return"/"===e.charAt(0)}function o(e,t){for(var n=t,r=n+1,a=e.length;r=0;f--){var p=i[f];"."===p?o(i,f):".."===p?(o(i,f),d++):d&&(o(i,f),d--)}if(!u)for(;d--;d)i.unshift("..");!u||""===i[0]||i[0]&&a(i[0])||i.unshift("");var m=i.join("/");return n&&"/"!==m.substr(-1)&&(m+="/"),m};var l=n(2177);function s(e){return"/"===e.charAt(0)?e:"/"+e}function u(e){return"/"===e.charAt(0)?e.substr(1):e}function c(e,t){return function(e,t){return 0===e.toLowerCase().indexOf(t.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(t.length))}(e,t)?e.substr(t.length):e}function d(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}function f(e){var t=e.pathname,n=e.search,r=e.hash,a=t||"/";return n&&"?"!==n&&(a+="?"===n.charAt(0)?n:"?"+n),r&&"#"!==r&&(a+="#"===r.charAt(0)?r:"#"+r),a}function p(e,t,n,a){var o;"string"==typeof e?(o=function(e){var t=e||"/",n="",r="",a=t.indexOf("#");-1!==a&&(r=t.substr(a),t=t.substr(0,a));var o=t.indexOf("?");return-1!==o&&(n=t.substr(o),t=t.substr(0,o)),{pathname:t,search:"?"===n?"":n,hash:"#"===r?"":r}}(e),o.state=t):(void 0===(o=(0,r.Z)({},e)).pathname&&(o.pathname=""),o.search?"?"!==o.search.charAt(0)&&(o.search="?"+o.search):o.search="",o.hash?"#"!==o.hash.charAt(0)&&(o.hash="#"+o.hash):o.hash="",void 0!==t&&void 0===o.state&&(o.state=t));try{o.pathname=decodeURI(o.pathname)}catch(l){throw l instanceof URIError?new URIError('Pathname "'+o.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.'):l}return n&&(o.key=n),a?o.pathname?"/"!==o.pathname.charAt(0)&&(o.pathname=i(o.pathname,a.pathname)):o.pathname=a.pathname:o.pathname||(o.pathname="/"),o}function m(){var e=null;var t=[];return{setPrompt:function(t){return e=t,function(){e===t&&(e=null)}},confirmTransitionTo:function(t,n,r,a){if(null!=e){var o="function"==typeof e?e(t,n):e;"string"==typeof o?"function"==typeof r?r(o,a):a(!0):a(!1!==o)}else a(!0)},appendListener:function(e){var n=!0;function r(){n&&e.apply(void 0,arguments)}return t.push(r),function(){n=!1,t=t.filter((function(e){return e!==r}))}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),r=0;rt?n.splice(t,n.length-t,a):n.push(a),d({action:r,location:a,index:t,entries:n})}}))},replace:function(e,t){var r="REPLACE",a=p(e,t,g(),w.location);c.confirmTransitionTo(a,r,n,(function(e){e&&(w.entries[w.index]=a,d({action:r,location:a}))}))},go:y,goBack:function(){y(-1)},goForward:function(){y(1)},canGo:function(e){var t=w.index+e;return t>=0&&t{"use strict";var r=n(9864),a={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},o={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},i={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},l={};function s(e){return r.isMemo(e)?i:l[e.$$typeof]||a}l[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},l[r.Memo]=i;var u=Object.defineProperty,c=Object.getOwnPropertyNames,d=Object.getOwnPropertySymbols,f=Object.getOwnPropertyDescriptor,p=Object.getPrototypeOf,m=Object.prototype;e.exports=function e(t,n,r){if("string"!=typeof n){if(m){var a=p(n);a&&a!==m&&e(t,a,r)}var i=c(n);d&&(i=i.concat(d(n)));for(var l=s(t),g=s(n),h=0;h{"use strict";e.exports=function(e,t,n,r,a,o,i,l){if(!e){var s;if(void 0===t)s=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var u=[n,r,a,o,i,l],c=0;(s=new Error(t.replace(/%s/g,(function(){return u[c++]})))).name="Invariant Violation"}throw s.framesToPop=1,s}}},5826:e=>{e.exports=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)}},2497:(e,t,n)=>{"use strict";n.r(t)},2295:(e,t,n)=>{"use strict";n.r(t)},4865:function(e,t,n){var r,a;r=function(){var e,t,n={version:"0.2.0"},r=n.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:'
'};function a(e,t,n){return en?n:e}function o(e){return 100*(-1+e)}function i(e,t,n){var a;return(a="translate3d"===r.positionUsing?{transform:"translate3d("+o(e)+"%,0,0)"}:"translate"===r.positionUsing?{transform:"translate("+o(e)+"%,0)"}:{"margin-left":o(e)+"%"}).transition="all "+t+"ms "+n,a}n.configure=function(e){var t,n;for(t in e)void 0!==(n=e[t])&&e.hasOwnProperty(t)&&(r[t]=n);return this},n.status=null,n.set=function(e){var t=n.isStarted();e=a(e,r.minimum,1),n.status=1===e?null:e;var o=n.render(!t),u=o.querySelector(r.barSelector),c=r.speed,d=r.easing;return o.offsetWidth,l((function(t){""===r.positionUsing&&(r.positionUsing=n.getPositioningCSS()),s(u,i(e,c,d)),1===e?(s(o,{transition:"none",opacity:1}),o.offsetWidth,setTimeout((function(){s(o,{transition:"all "+c+"ms linear",opacity:0}),setTimeout((function(){n.remove(),t()}),c)}),c)):setTimeout(t,c)})),this},n.isStarted=function(){return"number"==typeof n.status},n.start=function(){n.status||n.set(0);var e=function(){setTimeout((function(){n.status&&(n.trickle(),e())}),r.trickleSpeed)};return r.trickle&&e(),this},n.done=function(e){return e||n.status?n.inc(.3+.5*Math.random()).set(1):this},n.inc=function(e){var t=n.status;return t?("number"!=typeof e&&(e=(1-t)*a(Math.random()*t,.1,.95)),t=a(t+e,0,.994),n.set(t)):n.start()},n.trickle=function(){return n.inc(Math.random()*r.trickleRate)},e=0,t=0,n.promise=function(r){return r&&"resolved"!==r.state()?(0===t&&n.start(),e++,t++,r.always((function(){0==--t?(e=0,n.done()):n.set((e-t)/e)})),this):this},n.render=function(e){if(n.isRendered())return document.getElementById("nprogress");c(document.documentElement,"nprogress-busy");var t=document.createElement("div");t.id="nprogress",t.innerHTML=r.template;var a,i=t.querySelector(r.barSelector),l=e?"-100":o(n.status||0),u=document.querySelector(r.parent);return s(i,{transition:"all 0 linear",transform:"translate3d("+l+"%,0,0)"}),r.showSpinner||(a=t.querySelector(r.spinnerSelector))&&p(a),u!=document.body&&c(u,"nprogress-custom-parent"),u.appendChild(t),t},n.remove=function(){d(document.documentElement,"nprogress-busy"),d(document.querySelector(r.parent),"nprogress-custom-parent");var e=document.getElementById("nprogress");e&&p(e)},n.isRendered=function(){return!!document.getElementById("nprogress")},n.getPositioningCSS=function(){var e=document.body.style,t="WebkitTransform"in e?"Webkit":"MozTransform"in e?"Moz":"msTransform"in e?"ms":"OTransform"in e?"O":"";return t+"Perspective"in e?"translate3d":t+"Transform"in e?"translate":"margin"};var l=function(){var e=[];function t(){var n=e.shift();n&&n(t)}return function(n){e.push(n),1==e.length&&t()}}(),s=function(){var e=["Webkit","O","Moz","ms"],t={};function n(e){return e.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,(function(e,t){return t.toUpperCase()}))}function r(t){var n=document.body.style;if(t in n)return t;for(var r,a=e.length,o=t.charAt(0).toUpperCase()+t.slice(1);a--;)if((r=e[a]+o)in n)return r;return t}function a(e){return e=n(e),t[e]||(t[e]=r(e))}function o(e,t,n){t=a(t),e.style[t]=n}return function(e,t){var n,r,a=arguments;if(2==a.length)for(n in t)void 0!==(r=t[n])&&t.hasOwnProperty(n)&&o(e,n,r);else o(e,a[1],a[2])}}();function u(e,t){return("string"==typeof e?e:f(e)).indexOf(" "+t+" ")>=0}function c(e,t){var n=f(e),r=n+t;u(n,t)||(e.className=r.substring(1))}function d(e,t){var n,r=f(e);u(e,t)&&(n=r.replace(" "+t+" "," "),e.className=n.substring(1,n.length-1))}function f(e){return(" "+(e.className||"")+" ").replace(/\s+/gi," ")}function p(e){e&&e.parentNode&&e.parentNode.removeChild(e)}return n},void 0===(a="function"==typeof r?r.call(t,n,t,e):r)||(e.exports=a)},7418:e=>{"use strict";var t=Object.getOwnPropertySymbols,n=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable;function a(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(a){return!1}}()?Object.assign:function(e,o){for(var i,l,s=a(e),u=1;u{var r=n(5826);e.exports=p,e.exports.parse=o,e.exports.compile=function(e,t){return l(o(e,t),t)},e.exports.tokensToFunction=l,e.exports.tokensToRegExp=f;var a=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function o(e,t){for(var n,r=[],o=0,i=0,l="",c=t&&t.delimiter||"/";null!=(n=a.exec(e));){var d=n[0],f=n[1],p=n.index;if(l+=e.slice(i,p),i=p+d.length,f)l+=f[1];else{var m=e[i],g=n[2],h=n[3],b=n[4],v=n[5],y=n[6],w=n[7];l&&(r.push(l),l="");var k=null!=g&&null!=m&&m!==g,E="+"===y||"*"===y,S="?"===y||"*"===y,x=n[2]||c,_=b||v;r.push({name:h||o++,prefix:g||"",delimiter:x,optional:S,repeat:E,partial:k,asterisk:!!w,pattern:_?u(_):w?".*":"[^"+s(x)+"]+?"})}}return i{"use strict";n.d(t,{Z:()=>o});var r=function(){var e=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,n={},r={util:{encode:function e(t){return t instanceof a?new a(t.type,e(t.content),t.alias):Array.isArray(t)?t.map(e):t.replace(/&/g,"&").replace(/=d.reach);S+=E.value.length,E=E.next){var x=E.value;if(t.length>e.length)return;if(!(x instanceof a)){var _,C=1;if(v){if(!(_=o(k,S,e,b))||_.index>=e.length)break;var T=_.index,A=_.index+_[0].length,L=S;for(L+=E.value.length;T>=L;)L+=(E=E.next).value.length;if(S=L-=E.value.length,E.value instanceof a)continue;for(var R=E;R!==t.tail&&(Ld.reach&&(d.reach=D);var I=E.prev;if(N&&(I=s(t,I,N),S+=N.length),u(t,I,C),E=s(t,I,new a(f,h?r.tokenize(P,h):P,y,P)),O&&s(t,E,O),C>1){var M={cause:f+","+m,reach:D};i(e,t,n,E.prev,S,M),d&&M.reach>d.reach&&(d.reach=M.reach)}}}}}}function l(){var e={value:null,prev:null,next:null},t={value:null,prev:e,next:null};e.next=t,this.head=e,this.tail=t,this.length=0}function s(e,t,n){var r=t.next,a={value:n,prev:t,next:r};return t.next=a,r.prev=a,e.length++,a}function u(e,t,n){for(var r=t.next,a=0;a"+o.content+""},r}(),a=r;r.default=r,a.languages.markup={comment:{pattern://,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern://i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},a.languages.markup.tag.inside["attr-value"].inside.entity=a.languages.markup.entity,a.languages.markup.doctype.inside["internal-subset"].inside=a.languages.markup,a.hooks.add("wrap",(function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))})),Object.defineProperty(a.languages.markup.tag,"addInlined",{value:function(e,t){var n={};n["language-"+t]={pattern:/(^$)/i,lookbehind:!0,inside:a.languages[t]},n.cdata=/^$/i;var r={"included-cdata":{pattern://i,inside:n}};r["language-"+t]={pattern:/[\s\S]+/,inside:a.languages[t]};var o={};o[e]={pattern:RegExp(/(<__[^>]*>)(?:))*\]\]>|(?!)/.source.replace(/__/g,(function(){return e})),"i"),lookbehind:!0,greedy:!0,inside:r},a.languages.insertBefore("markup","cdata",o)}}),Object.defineProperty(a.languages.markup.tag,"addAttribute",{value:function(e,t){a.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[t,"language-"+t],inside:a.languages[t]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),a.languages.html=a.languages.markup,a.languages.mathml=a.languages.markup,a.languages.svg=a.languages.markup,a.languages.xml=a.languages.extend("markup",{}),a.languages.ssml=a.languages.xml,a.languages.atom=a.languages.xml,a.languages.rss=a.languages.xml,function(e){var t="\\b(?:BASH|BASHOPTS|BASH_ALIASES|BASH_ARGC|BASH_ARGV|BASH_CMDS|BASH_COMPLETION_COMPAT_DIR|BASH_LINENO|BASH_REMATCH|BASH_SOURCE|BASH_VERSINFO|BASH_VERSION|COLORTERM|COLUMNS|COMP_WORDBREAKS|DBUS_SESSION_BUS_ADDRESS|DEFAULTS_PATH|DESKTOP_SESSION|DIRSTACK|DISPLAY|EUID|GDMSESSION|GDM_LANG|GNOME_KEYRING_CONTROL|GNOME_KEYRING_PID|GPG_AGENT_INFO|GROUPS|HISTCONTROL|HISTFILE|HISTFILESIZE|HISTSIZE|HOME|HOSTNAME|HOSTTYPE|IFS|INSTANCE|JOB|LANG|LANGUAGE|LC_ADDRESS|LC_ALL|LC_IDENTIFICATION|LC_MEASUREMENT|LC_MONETARY|LC_NAME|LC_NUMERIC|LC_PAPER|LC_TELEPHONE|LC_TIME|LESSCLOSE|LESSOPEN|LINES|LOGNAME|LS_COLORS|MACHTYPE|MAILCHECK|MANDATORY_PATH|NO_AT_BRIDGE|OLDPWD|OPTERR|OPTIND|ORBIT_SOCKETDIR|OSTYPE|PAPERSIZE|PATH|PIPESTATUS|PPID|PS1|PS2|PS3|PS4|PWD|RANDOM|REPLY|SECONDS|SELINUX_INIT|SESSION|SESSIONTYPE|SESSION_MANAGER|SHELL|SHELLOPTS|SHLVL|SSH_AUTH_SOCK|TERM|UID|UPSTART_EVENTS|UPSTART_INSTANCE|UPSTART_JOB|UPSTART_SESSION|USER|WINDOWID|XAUTHORITY|XDG_CONFIG_DIRS|XDG_CURRENT_DESKTOP|XDG_DATA_DIRS|XDG_GREETER_DATA_DIR|XDG_MENU_PREFIX|XDG_RUNTIME_DIR|XDG_SEAT|XDG_SEAT_PATH|XDG_SESSION_DESKTOP|XDG_SESSION_ID|XDG_SESSION_PATH|XDG_SESSION_TYPE|XDG_VTNR|XMODIFIERS)\\b",n={pattern:/(^(["']?)\w+\2)[ \t]+\S.*/,lookbehind:!0,alias:"punctuation",inside:null},r={bash:n,environment:{pattern:RegExp("\\$"+t),alias:"constant"},variable:[{pattern:/\$?\(\([\s\S]+?\)\)/,greedy:!0,inside:{variable:[{pattern:/(^\$\(\([\s\S]+)\)\)/,lookbehind:!0},/^\$\(\(/],number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee]-?\d+)?/,operator:/--|\+\+|\*\*=?|<<=?|>>=?|&&|\|\||[=!+\-*/%<>^&|]=?|[?~:]/,punctuation:/\(\(?|\)\)?|,|;/}},{pattern:/\$\((?:\([^)]+\)|[^()])+\)|`[^`]+`/,greedy:!0,inside:{variable:/^\$\(|^`|\)$|`$/}},{pattern:/\$\{[^}]+\}/,greedy:!0,inside:{operator:/:[-=?+]?|[!\/]|##?|%%?|\^\^?|,,?/,punctuation:/[\[\]]/,environment:{pattern:RegExp("(\\{)"+t),lookbehind:!0,alias:"constant"}}},/\$(?:\w+|[#?*!@$])/],entity:/\\(?:[abceEfnrtv\\"]|O?[0-7]{1,3}|U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{1,2})/};e.languages.bash={shebang:{pattern:/^#!\s*\/.*/,alias:"important"},comment:{pattern:/(^|[^"{\\$])#.*/,lookbehind:!0},"function-name":[{pattern:/(\bfunction\s+)[\w-]+(?=(?:\s*\(?:\s*\))?\s*\{)/,lookbehind:!0,alias:"function"},{pattern:/\b[\w-]+(?=\s*\(\s*\)\s*\{)/,alias:"function"}],"for-or-select":{pattern:/(\b(?:for|select)\s+)\w+(?=\s+in\s)/,alias:"variable",lookbehind:!0},"assign-left":{pattern:/(^|[\s;|&]|[<>]\()\w+(?=\+?=)/,inside:{environment:{pattern:RegExp("(^|[\\s;|&]|[<>]\\()"+t),lookbehind:!0,alias:"constant"}},alias:"variable",lookbehind:!0},string:[{pattern:/((?:^|[^<])<<-?\s*)(\w+)\s[\s\S]*?(?:\r?\n|\r)\2/,lookbehind:!0,greedy:!0,inside:r},{pattern:/((?:^|[^<])<<-?\s*)(["'])(\w+)\2\s[\s\S]*?(?:\r?\n|\r)\3/,lookbehind:!0,greedy:!0,inside:{bash:n}},{pattern:/(^|[^\\](?:\\\\)*)"(?:\\[\s\S]|\$\([^)]+\)|\$(?!\()|`[^`]+`|[^"\\`$])*"/,lookbehind:!0,greedy:!0,inside:r},{pattern:/(^|[^$\\])'[^']*'/,lookbehind:!0,greedy:!0},{pattern:/\$'(?:[^'\\]|\\[\s\S])*'/,greedy:!0,inside:{entity:r.entity}}],environment:{pattern:RegExp("\\$?"+t),alias:"constant"},variable:r.variable,function:{pattern:/(^|[\s;|&]|[<>]\()(?:add|apropos|apt|apt-cache|apt-get|aptitude|aspell|automysqlbackup|awk|basename|bash|bc|bconsole|bg|bzip2|cal|cat|cfdisk|chgrp|chkconfig|chmod|chown|chroot|cksum|clear|cmp|column|comm|composer|cp|cron|crontab|csplit|curl|cut|date|dc|dd|ddrescue|debootstrap|df|diff|diff3|dig|dir|dircolors|dirname|dirs|dmesg|docker|docker-compose|du|egrep|eject|env|ethtool|expand|expect|expr|fdformat|fdisk|fg|fgrep|file|find|fmt|fold|format|free|fsck|ftp|fuser|gawk|git|gparted|grep|groupadd|groupdel|groupmod|groups|grub-mkconfig|gzip|halt|head|hg|history|host|hostname|htop|iconv|id|ifconfig|ifdown|ifup|import|install|ip|jobs|join|kill|killall|less|link|ln|locate|logname|logrotate|look|lpc|lpr|lprint|lprintd|lprintq|lprm|ls|lsof|lynx|make|man|mc|mdadm|mkconfig|mkdir|mke2fs|mkfifo|mkfs|mkisofs|mknod|mkswap|mmv|more|most|mount|mtools|mtr|mutt|mv|nano|nc|netstat|nice|nl|node|nohup|notify-send|npm|nslookup|op|open|parted|passwd|paste|pathchk|ping|pkill|pnpm|podman|podman-compose|popd|pr|printcap|printenv|ps|pushd|pv|quota|quotacheck|quotactl|ram|rar|rcp|reboot|remsync|rename|renice|rev|rm|rmdir|rpm|rsync|scp|screen|sdiff|sed|sendmail|seq|service|sftp|sh|shellcheck|shuf|shutdown|sleep|slocate|sort|split|ssh|stat|strace|su|sudo|sum|suspend|swapon|sync|tac|tail|tar|tee|time|timeout|top|touch|tr|traceroute|tsort|tty|umount|uname|unexpand|uniq|units|unrar|unshar|unzip|update-grub|uptime|useradd|userdel|usermod|users|uudecode|uuencode|v|vcpkg|vdir|vi|vim|virsh|vmstat|wait|watch|wc|wget|whereis|which|who|whoami|write|xargs|xdg-open|yarn|yes|zenity|zip|zsh|zypper)(?=$|[)\s;|&])/,lookbehind:!0},keyword:{pattern:/(^|[\s;|&]|[<>]\()(?:case|do|done|elif|else|esac|fi|for|function|if|in|select|then|until|while)(?=$|[)\s;|&])/,lookbehind:!0},builtin:{pattern:/(^|[\s;|&]|[<>]\()(?:\.|:|alias|bind|break|builtin|caller|cd|command|continue|declare|echo|enable|eval|exec|exit|export|getopts|hash|help|let|local|logout|mapfile|printf|pwd|read|readarray|readonly|return|set|shift|shopt|source|test|times|trap|type|typeset|ulimit|umask|unalias|unset)(?=$|[)\s;|&])/,lookbehind:!0,alias:"class-name"},boolean:{pattern:/(^|[\s;|&]|[<>]\()(?:false|true)(?=$|[)\s;|&])/,lookbehind:!0},"file-descriptor":{pattern:/\B&\d\b/,alias:"important"},operator:{pattern:/\d?<>|>\||\+=|=[=~]?|!=?|<<[<-]?|[&\d]?>>|\d[<>]&?|[<>][&=]?|&[>&]?|\|[&|]?/,inside:{"file-descriptor":{pattern:/^\d/,alias:"important"}}},punctuation:/\$?\(\(?|\)\)?|\.\.|[{}[\];\\]/,number:{pattern:/(^|\s)(?:[1-9]\d*|0)(?:[.,]\d+)?\b/,lookbehind:!0}},n.inside=e.languages.bash;for(var a=["comment","function-name","for-or-select","assign-left","string","environment","function","keyword","builtin","boolean","file-descriptor","operator","punctuation","number"],o=r.variable[1].inside,i=0;i]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},a.languages.c=a.languages.extend("clike",{comment:{pattern:/\/\/(?:[^\r\n\\]|\\(?:\r\n?|\n|(?![\r\n])))*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},string:{pattern:/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,greedy:!0},"class-name":{pattern:/(\b(?:enum|struct)\s+(?:__attribute__\s*\(\([\s\S]*?\)\)\s*)?)\w+|\b[a-z]\w*_t\b/,lookbehind:!0},keyword:/\b(?:_Alignas|_Alignof|_Atomic|_Bool|_Complex|_Generic|_Imaginary|_Noreturn|_Static_assert|_Thread_local|__attribute__|asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|inline|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|typeof|union|unsigned|void|volatile|while)\b/,function:/\b[a-z_]\w*(?=\s*\()/i,number:/(?:\b0x(?:[\da-f]+(?:\.[\da-f]*)?|\.[\da-f]+)(?:p[+-]?\d+)?|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?)[ful]{0,4}/i,operator:/>>=?|<<=?|->|([-+&|:])\1|[?:~]|[-+*/%&|^!=<>]=?/}),a.languages.insertBefore("c","string",{char:{pattern:/'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n]){0,32}'/,greedy:!0}}),a.languages.insertBefore("c","string",{macro:{pattern:/(^[\t ]*)#\s*[a-z](?:[^\r\n\\/]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|\\(?:\r\n|[\s\S]))*/im,lookbehind:!0,greedy:!0,alias:"property",inside:{string:[{pattern:/^(#\s*include\s*)<[^>]+>/,lookbehind:!0},a.languages.c.string],char:a.languages.c.char,comment:a.languages.c.comment,"macro-name":[{pattern:/(^#\s*define\s+)\w+\b(?!\()/i,lookbehind:!0},{pattern:/(^#\s*define\s+)\w+\b(?=\()/i,lookbehind:!0,alias:"function"}],directive:{pattern:/^(#\s*)[a-z]+/,lookbehind:!0,alias:"keyword"},"directive-hash":/^#/,punctuation:/##|\\(?=[\r\n])/,expression:{pattern:/\S[\s\S]*/,inside:a.languages.c}}}}),a.languages.insertBefore("c","function",{constant:/\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\b/}),delete a.languages.c.boolean,function(e){var t=/\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,n=/\b(?!)\w+(?:\s*\.\s*\w+)*\b/.source.replace(//g,(function(){return t.source}));e.languages.cpp=e.languages.extend("c",{"class-name":[{pattern:RegExp(/(\b(?:class|concept|enum|struct|typename)\s+)(?!)\w+/.source.replace(//g,(function(){return t.source}))),lookbehind:!0},/\b[A-Z]\w*(?=\s*::\s*\w+\s*\()/,/\b[A-Z_]\w*(?=\s*::\s*~\w+\s*\()/i,/\b\w+(?=\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>\s*::\s*\w+\s*\()/],keyword:t,number:{pattern:/(?:\b0b[01']+|\b0x(?:[\da-f']+(?:\.[\da-f']*)?|\.[\da-f']+)(?:p[+-]?[\d']+)?|(?:\b[\d']+(?:\.[\d']*)?|\B\.[\d']+)(?:e[+-]?[\d']+)?)[ful]{0,4}/i,greedy:!0},operator:/>>=?|<<=?|->|--|\+\+|&&|\|\||[?:~]|<=>|[-+*/%&|^!=<>]=?|\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/,boolean:/\b(?:false|true)\b/}),e.languages.insertBefore("cpp","string",{module:{pattern:RegExp(/(\b(?:import|module)\s+)/.source+"(?:"+/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|<[^<>\r\n]*>/.source+"|"+/(?:\s*:\s*)?|:\s*/.source.replace(//g,(function(){return n}))+")"),lookbehind:!0,greedy:!0,inside:{string:/^[<"][\s\S]+/,operator:/:/,punctuation:/\./}},"raw-string":{pattern:/R"([^()\\ ]{0,16})\([\s\S]*?\)\1"/,alias:"string",greedy:!0}}),e.languages.insertBefore("cpp","keyword",{"generic-function":{pattern:/\b(?!operator\b)[a-z_]\w*\s*<(?:[^<>]|<[^<>]*>)*>(?=\s*\()/i,inside:{function:/^\w+/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:e.languages.cpp}}}}),e.languages.insertBefore("cpp","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}}),e.languages.insertBefore("cpp","class-name",{"base-clause":{pattern:/(\b(?:class|struct)\s+\w+\s*:\s*)[^;{}"'\s]+(?:\s+[^;{}"'\s]+)*(?=\s*[;{])/,lookbehind:!0,greedy:!0,inside:e.languages.extend("cpp",{})}}),e.languages.insertBefore("inside","double-colon",{"class-name":/\b[a-z_]\w*\b(?!\s*::)/i},e.languages.cpp["base-clause"])}(a),function(e){var t=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-](?:[^;{\s]|\s+(?![\s{]))*(?:;|(?=\s*\{))/,inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+t.source+"|"+/(?:[^\\\r\n()"']|\\[\s\S])*/.source+")\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+t.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+t.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:t,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var n=e.languages.markup;n&&(n.tag.addInlined("style","css"),n.tag.addAttribute("style","css"))}(a),function(e){var t,n=/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/;e.languages.css.selector={pattern:e.languages.css.selector.pattern,lookbehind:!0,inside:t={"pseudo-element":/:(?:after|before|first-letter|first-line|selection)|::[-\w]+/,"pseudo-class":/:[-\w]+/,class:/\.[-\w]+/,id:/#[-\w]+/,attribute:{pattern:RegExp("\\[(?:[^[\\]\"']|"+n.source+")*\\]"),greedy:!0,inside:{punctuation:/^\[|\]$/,"case-sensitivity":{pattern:/(\s)[si]$/i,lookbehind:!0,alias:"keyword"},namespace:{pattern:/^(\s*)(?:(?!\s)[-*\w\xA0-\uFFFF])*\|(?!=)/,lookbehind:!0,inside:{punctuation:/\|$/}},"attr-name":{pattern:/^(\s*)(?:(?!\s)[-\w\xA0-\uFFFF])+/,lookbehind:!0},"attr-value":[n,{pattern:/(=\s*)(?:(?!\s)[-\w\xA0-\uFFFF])+(?=\s*$)/,lookbehind:!0}],operator:/[|~*^$]?=/}},"n-th":[{pattern:/(\(\s*)[+-]?\d*[\dn](?:\s*[+-]\s*\d+)?(?=\s*\))/,lookbehind:!0,inside:{number:/[\dn]+/,operator:/[+-]/}},{pattern:/(\(\s*)(?:even|odd)(?=\s*\))/i,lookbehind:!0}],combinator:/>|\+|~|\|\|/,punctuation:/[(),]/}},e.languages.css.atrule.inside["selector-function-argument"].inside=t,e.languages.insertBefore("css","property",{variable:{pattern:/(^|[^-\w\xA0-\uFFFF])--(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*/i,lookbehind:!0}});var r={pattern:/(\b\d+)(?:%|[a-z]+(?![\w-]))/,lookbehind:!0},a={pattern:/(^|[^\w.-])-?(?:\d+(?:\.\d+)?|\.\d+)/,lookbehind:!0};e.languages.insertBefore("css","function",{operator:{pattern:/(\s)[+\-*\/](?=\s)/,lookbehind:!0},hexcode:{pattern:/\B#[\da-f]{3,8}\b/i,alias:"color"},color:[{pattern:/(^|[^\w-])(?:AliceBlue|AntiqueWhite|Aqua|Aquamarine|Azure|Beige|Bisque|Black|BlanchedAlmond|Blue|BlueViolet|Brown|BurlyWood|CadetBlue|Chartreuse|Chocolate|Coral|CornflowerBlue|Cornsilk|Crimson|Cyan|DarkBlue|DarkCyan|DarkGoldenRod|DarkGr[ae]y|DarkGreen|DarkKhaki|DarkMagenta|DarkOliveGreen|DarkOrange|DarkOrchid|DarkRed|DarkSalmon|DarkSeaGreen|DarkSlateBlue|DarkSlateGr[ae]y|DarkTurquoise|DarkViolet|DeepPink|DeepSkyBlue|DimGr[ae]y|DodgerBlue|FireBrick|FloralWhite|ForestGreen|Fuchsia|Gainsboro|GhostWhite|Gold|GoldenRod|Gr[ae]y|Green|GreenYellow|HoneyDew|HotPink|IndianRed|Indigo|Ivory|Khaki|Lavender|LavenderBlush|LawnGreen|LemonChiffon|LightBlue|LightCoral|LightCyan|LightGoldenRodYellow|LightGr[ae]y|LightGreen|LightPink|LightSalmon|LightSeaGreen|LightSkyBlue|LightSlateGr[ae]y|LightSteelBlue|LightYellow|Lime|LimeGreen|Linen|Magenta|Maroon|MediumAquaMarine|MediumBlue|MediumOrchid|MediumPurple|MediumSeaGreen|MediumSlateBlue|MediumSpringGreen|MediumTurquoise|MediumVioletRed|MidnightBlue|MintCream|MistyRose|Moccasin|NavajoWhite|Navy|OldLace|Olive|OliveDrab|Orange|OrangeRed|Orchid|PaleGoldenRod|PaleGreen|PaleTurquoise|PaleVioletRed|PapayaWhip|PeachPuff|Peru|Pink|Plum|PowderBlue|Purple|Red|RosyBrown|RoyalBlue|SaddleBrown|Salmon|SandyBrown|SeaGreen|SeaShell|Sienna|Silver|SkyBlue|SlateBlue|SlateGr[ae]y|Snow|SpringGreen|SteelBlue|Tan|Teal|Thistle|Tomato|Transparent|Turquoise|Violet|Wheat|White|WhiteSmoke|Yellow|YellowGreen)(?![\w-])/i,lookbehind:!0},{pattern:/\b(?:hsl|rgb)\(\s*\d{1,3}\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*\)\B|\b(?:hsl|rgb)a\(\s*\d{1,3}\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*,\s*(?:0|0?\.\d+|1)\s*\)\B/i,inside:{unit:r,number:a,function:/[\w-]+(?=\()/,punctuation:/[(),]/}}],entity:/\\[\da-f]{1,8}/i,unit:r,number:a})}(a),a.languages.javascript=a.languages.extend("clike",{"class-name":[a.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp(/(^|[^\w$])/.source+"(?:"+/NaN|Infinity/.source+"|"+/0[bB][01]+(?:_[01]+)*n?/.source+"|"+/0[oO][0-7]+(?:_[0-7]+)*n?/.source+"|"+/0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source+"|"+/\d+(?:_\d+)*n/.source+"|"+/(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/.source+")"+/(?![\w$])/.source),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),a.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,a.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)\/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/,lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:a.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:a.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:a.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:a.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:a.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),a.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:a.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),a.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),a.languages.markup&&(a.languages.markup.tag.addInlined("script","javascript"),a.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),a.languages.js=a.languages.javascript,function(e){var t=/#(?!\{).+/,n={pattern:/#\{[^}]+\}/,alias:"variable"};e.languages.coffeescript=e.languages.extend("javascript",{comment:t,string:[{pattern:/'(?:\\[\s\S]|[^\\'])*'/,greedy:!0},{pattern:/"(?:\\[\s\S]|[^\\"])*"/,greedy:!0,inside:{interpolation:n}}],keyword:/\b(?:and|break|by|catch|class|continue|debugger|delete|do|each|else|extend|extends|false|finally|for|if|in|instanceof|is|isnt|let|loop|namespace|new|no|not|null|of|off|on|or|own|return|super|switch|then|this|throw|true|try|typeof|undefined|unless|until|when|while|window|with|yes|yield)\b/,"class-member":{pattern:/@(?!\d)\w+/,alias:"variable"}}),e.languages.insertBefore("coffeescript","comment",{"multiline-comment":{pattern:/###[\s\S]+?###/,alias:"comment"},"block-regex":{pattern:/\/{3}[\s\S]*?\/{3}/,alias:"regex",inside:{comment:t,interpolation:n}}}),e.languages.insertBefore("coffeescript","string",{"inline-javascript":{pattern:/`(?:\\[\s\S]|[^\\`])*`/,inside:{delimiter:{pattern:/^`|`$/,alias:"punctuation"},script:{pattern:/[\s\S]+/,alias:"language-javascript",inside:e.languages.javascript}}},"multiline-string":[{pattern:/'''[\s\S]*?'''/,greedy:!0,alias:"string"},{pattern:/"""[\s\S]*?"""/,greedy:!0,alias:"string",inside:{interpolation:n}}]}),e.languages.insertBefore("coffeescript","keyword",{property:/(?!\d)\w+(?=\s*:(?!:))/}),delete e.languages.coffeescript["template-string"],e.languages.coffee=e.languages.coffeescript}(a),function(e){var t=/[*&][^\s[\]{},]+/,n=/!(?:<[\w\-%#;/?:@&=+$,.!~*'()[\]]+>|(?:[a-zA-Z\d-]*!)?[\w\-%#;/?:@&=+$.~*'()]+)?/,r="(?:"+n.source+"(?:[ \t]+"+t.source+")?|"+t.source+"(?:[ \t]+"+n.source+")?)",a=/(?:[^\s\x00-\x08\x0e-\x1f!"#%&'*,\-:>?@[\]`{|}\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]|[?:-])(?:[ \t]*(?:(?![#:])|:))*/.source.replace(//g,(function(){return/[^\s\x00-\x08\x0e-\x1f,[\]{}\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]/.source})),o=/"(?:[^"\\\r\n]|\\.)*"|'(?:[^'\\\r\n]|\\.)*'/.source;function i(e,t){t=(t||"").replace(/m/g,"")+"m";var n=/([:\-,[{]\s*(?:\s<>[ \t]+)?)(?:<>)(?=[ \t]*(?:$|,|\]|\}|(?:[\r\n]\s*)?#))/.source.replace(/<>/g,(function(){return r})).replace(/<>/g,(function(){return e}));return RegExp(n,t)}e.languages.yaml={scalar:{pattern:RegExp(/([\-:]\s*(?:\s<>[ \t]+)?[|>])[ \t]*(?:((?:\r?\n|\r)[ \t]+)\S[^\r\n]*(?:\2[^\r\n]+)*)/.source.replace(/<>/g,(function(){return r}))),lookbehind:!0,alias:"string"},comment:/#.*/,key:{pattern:RegExp(/((?:^|[:\-,[{\r\n?])[ \t]*(?:<>[ \t]+)?)<>(?=\s*:\s)/.source.replace(/<>/g,(function(){return r})).replace(/<>/g,(function(){return"(?:"+a+"|"+o+")"}))),lookbehind:!0,greedy:!0,alias:"atrule"},directive:{pattern:/(^[ \t]*)%.+/m,lookbehind:!0,alias:"important"},datetime:{pattern:i(/\d{4}-\d\d?-\d\d?(?:[tT]|[ \t]+)\d\d?:\d{2}:\d{2}(?:\.\d*)?(?:[ \t]*(?:Z|[-+]\d\d?(?::\d{2})?))?|\d{4}-\d{2}-\d{2}|\d\d?:\d{2}(?::\d{2}(?:\.\d*)?)?/.source),lookbehind:!0,alias:"number"},boolean:{pattern:i(/false|true/.source,"i"),lookbehind:!0,alias:"important"},null:{pattern:i(/null|~/.source,"i"),lookbehind:!0,alias:"important"},string:{pattern:i(o),lookbehind:!0,greedy:!0},number:{pattern:i(/[+-]?(?:0x[\da-f]+|0o[0-7]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|\.inf|\.nan)/.source,"i"),lookbehind:!0},tag:n,important:t,punctuation:/---|[:[\]{}\-,|>?]|\.\.\./},e.languages.yml=e.languages.yaml}(a),function(e){var t=/(?:\\.|[^\\\n\r]|(?:\n|\r\n?)(?![\r\n]))/.source;function n(e){return e=e.replace(//g,(function(){return t})),RegExp(/((?:^|[^\\])(?:\\{2})*)/.source+"(?:"+e+")")}var r=/(?:\\.|``(?:[^`\r\n]|`(?!`))+``|`[^`\r\n]+`|[^\\|\r\n`])+/.source,a=/\|?__(?:\|__)+\|?(?:(?:\n|\r\n?)|(?![\s\S]))/.source.replace(/__/g,(function(){return r})),o=/\|?[ \t]*:?-{3,}:?[ \t]*(?:\|[ \t]*:?-{3,}:?[ \t]*)+\|?(?:\n|\r\n?)/.source;e.languages.markdown=e.languages.extend("markup",{}),e.languages.insertBefore("markdown","prolog",{"front-matter-block":{pattern:/(^(?:\s*[\r\n])?)---(?!.)[\s\S]*?[\r\n]---(?!.)/,lookbehind:!0,greedy:!0,inside:{punctuation:/^---|---$/,"front-matter":{pattern:/\S+(?:\s+\S+)*/,alias:["yaml","language-yaml"],inside:e.languages.yaml}}},blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},table:{pattern:RegExp("^"+a+o+"(?:"+a+")*","m"),inside:{"table-data-rows":{pattern:RegExp("^("+a+o+")(?:"+a+")*$"),lookbehind:!0,inside:{"table-data":{pattern:RegExp(r),inside:e.languages.markdown},punctuation:/\|/}},"table-line":{pattern:RegExp("^("+a+")"+o+"$"),lookbehind:!0,inside:{punctuation:/\||:?-{3,}:?/}},"table-header-row":{pattern:RegExp("^"+a+"$"),inside:{"table-header":{pattern:RegExp(r),alias:"important",inside:e.languages.markdown},punctuation:/\|/}}}},code:[{pattern:/((?:^|\n)[ \t]*\n|(?:^|\r\n?)[ \t]*\r\n?)(?: {4}|\t).+(?:(?:\n|\r\n?)(?: {4}|\t).+)*/,lookbehind:!0,alias:"keyword"},{pattern:/^```[\s\S]*?^```$/m,greedy:!0,inside:{"code-block":{pattern:/^(```.*(?:\n|\r\n?))[\s\S]+?(?=(?:\n|\r\n?)^```$)/m,lookbehind:!0},"code-language":{pattern:/^(```).+/,lookbehind:!0},punctuation:/```/}}],title:[{pattern:/\S.*(?:\n|\r\n?)(?:==+|--+)(?=[ \t]*$)/m,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])(?:[\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:n(/\b__(?:(?!_)|_(?:(?!_))+_)+__\b|\*\*(?:(?!\*)|\*(?:(?!\*))+\*)+\*\*/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^..)[\s\S]+(?=..$)/,lookbehind:!0,inside:{}},punctuation:/\*\*|__/}},italic:{pattern:n(/\b_(?:(?!_)|__(?:(?!_))+__)+_\b|\*(?:(?!\*)|\*\*(?:(?!\*))+\*\*)+\*/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^.)[\s\S]+(?=.$)/,lookbehind:!0,inside:{}},punctuation:/[*_]/}},strike:{pattern:n(/(~~?)(?:(?!~))+\2/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^~~?)[\s\S]+(?=\1$)/,lookbehind:!0,inside:{}},punctuation:/~~?/}},"code-snippet":{pattern:/(^|[^\\`])(?:``[^`\r\n]+(?:`[^`\r\n]+)*``(?!`)|`[^`\r\n]+`(?!`))/,lookbehind:!0,greedy:!0,alias:["code","keyword"]},url:{pattern:n(/!?\[(?:(?!\]))+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)|[ \t]?\[(?:(?!\]))+\])/.source),lookbehind:!0,greedy:!0,inside:{operator:/^!/,content:{pattern:/(^\[)[^\]]+(?=\])/,lookbehind:!0,inside:{}},variable:{pattern:/(^\][ \t]?\[)[^\]]+(?=\]$)/,lookbehind:!0},url:{pattern:/(^\]\()[^\s)]+/,lookbehind:!0},string:{pattern:/(^[ \t]+)"(?:\\.|[^"\\])*"(?=\)$)/,lookbehind:!0}}}}),["url","bold","italic","strike"].forEach((function(t){["url","bold","italic","strike","code-snippet"].forEach((function(n){t!==n&&(e.languages.markdown[t].inside.content.inside[n]=e.languages.markdown[n])}))})),e.hooks.add("after-tokenize",(function(e){"markdown"!==e.language&&"md"!==e.language||function e(t){if(t&&"string"!=typeof t)for(var n=0,r=t.length;n",quot:'"'},s=String.fromCodePoint||String.fromCharCode;e.languages.md=e.languages.markdown}(a),a.languages.graphql={comment:/#.*/,description:{pattern:/(?:"""(?:[^"]|(?!""")")*"""|"(?:\\.|[^\\"\r\n])*")(?=\s*[a-z_])/i,greedy:!0,alias:"string",inside:{"language-markdown":{pattern:/(^"(?:"")?)(?!\1)[\s\S]+(?=\1$)/,lookbehind:!0,inside:a.languages.markdown}}},string:{pattern:/"""(?:[^"]|(?!""")")*"""|"(?:\\.|[^\\"\r\n])*"/,greedy:!0},number:/(?:\B-|\b)\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,boolean:/\b(?:false|true)\b/,variable:/\$[a-z_]\w*/i,directive:{pattern:/@[a-z_]\w*/i,alias:"function"},"attr-name":{pattern:/\b[a-z_]\w*(?=\s*(?:\((?:[^()"]|"(?:\\.|[^\\"\r\n])*")*\))?:)/i,greedy:!0},"atom-input":{pattern:/\b[A-Z]\w*Input\b/,alias:"class-name"},scalar:/\b(?:Boolean|Float|ID|Int|String)\b/,constant:/\b[A-Z][A-Z_\d]*\b/,"class-name":{pattern:/(\b(?:enum|implements|interface|on|scalar|type|union)\s+|&\s*|:\s*|\[)[A-Z_]\w*/,lookbehind:!0},fragment:{pattern:/(\bfragment\s+|\.{3}\s*(?!on\b))[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},"definition-mutation":{pattern:/(\bmutation\s+)[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},"definition-query":{pattern:/(\bquery\s+)[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},keyword:/\b(?:directive|enum|extend|fragment|implements|input|interface|mutation|on|query|repeatable|scalar|schema|subscription|type|union)\b/,operator:/[!=|&]|\.{3}/,"property-query":/\w+(?=\s*\()/,object:/\w+(?=\s*\{)/,punctuation:/[!(){}\[\]:=,]/,property:/\w+/},a.hooks.add("after-tokenize",(function(e){if("graphql"===e.language)for(var t=e.tokens.filter((function(e){return"string"!=typeof e&&"comment"!==e.type&&"scalar"!==e.type})),n=0;n0)){var l=f(/^\{$/,/^\}$/);if(-1===l)continue;for(var s=n;s=0&&p(u,"variable-input")}}}}function c(e){return t[n+e]}function d(e,t){t=t||0;for(var n=0;n?|<|>)?|>[>=]?|\b(?:AND|BETWEEN|DIV|ILIKE|IN|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\b/i,punctuation:/[;[\]()`,.]/},function(e){var t=e.languages.javascript["template-string"],n=t.pattern.source,r=t.inside.interpolation,a=r.inside["interpolation-punctuation"],o=r.pattern.source;function i(t,r){if(e.languages[t])return{pattern:RegExp("((?:"+r+")\\s*)"+n),lookbehind:!0,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},"embedded-code":{pattern:/[\s\S]+/,alias:t}}}}function l(e,t){return"___"+t.toUpperCase()+"_"+e+"___"}function s(t,n,r){var a={code:t,grammar:n,language:r};return e.hooks.run("before-tokenize",a),a.tokens=e.tokenize(a.code,a.grammar),e.hooks.run("after-tokenize",a),a.tokens}function u(t){var n={};n["interpolation-punctuation"]=a;var o=e.tokenize(t,n);if(3===o.length){var i=[1,1];i.push.apply(i,s(o[1],e.languages.javascript,"javascript")),o.splice.apply(o,i)}return new e.Token("interpolation",o,r.alias,t)}function c(t,n,r){var a=e.tokenize(t,{interpolation:{pattern:RegExp(o),lookbehind:!0}}),i=0,c={},d=s(a.map((function(e){if("string"==typeof e)return e;for(var n,a=e.content;-1!==t.indexOf(n=l(i++,r)););return c[n]=a,n})).join(""),n,r),f=Object.keys(c);return i=0,function e(t){for(var n=0;n=f.length)return;var r=t[n];if("string"==typeof r||"string"==typeof r.content){var a=f[i],o="string"==typeof r?r:r.content,l=o.indexOf(a);if(-1!==l){++i;var s=o.substring(0,l),d=u(c[a]),p=o.substring(l+a.length),m=[];if(s&&m.push(s),m.push(d),p){var g=[p];e(g),m.push.apply(m,g)}"string"==typeof r?(t.splice.apply(t,[n,1].concat(m)),n+=m.length-1):r.content=m}}else{var h=r.content;Array.isArray(h)?e(h):e([h])}}}(d),new e.Token(r,d,"language-"+r,t)}e.languages.javascript["template-string"]=[i("css",/\b(?:styled(?:\([^)]*\))?(?:\s*\.\s*\w+(?:\([^)]*\))*)*|css(?:\s*\.\s*(?:global|resolve))?|createGlobalStyle|keyframes)/.source),i("html",/\bhtml|\.\s*(?:inner|outer)HTML\s*\+?=/.source),i("svg",/\bsvg/.source),i("markdown",/\b(?:markdown|md)/.source),i("graphql",/\b(?:gql|graphql(?:\s*\.\s*experimental)?)/.source),i("sql",/\bsql/.source),t].filter(Boolean);var d={javascript:!0,js:!0,typescript:!0,ts:!0,jsx:!0,tsx:!0};function f(e){return"string"==typeof e?e:Array.isArray(e)?e.map(f).join(""):f(e.content)}e.hooks.add("after-tokenize",(function(t){t.language in d&&function t(n){for(var r=0,a=n.length;r]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/}),e.languages.typescript.keyword.push(/\b(?:abstract|declare|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete e.languages.typescript.parameter,delete e.languages.typescript["literal-property"];var t=e.languages.extend("typescript",{});delete t["class-name"],e.languages.typescript["class-name"].inside=t,e.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:t}}}}),e.languages.ts=e.languages.typescript}(a),function(e){function t(e,t){return RegExp(e.replace(//g,(function(){return/(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/.source})),t)}e.languages.insertBefore("javascript","function-variable",{"method-variable":{pattern:RegExp("(\\.\\s*)"+e.languages.javascript["function-variable"].pattern.source),lookbehind:!0,alias:["function-variable","method","function","property-access"]}}),e.languages.insertBefore("javascript","function",{method:{pattern:RegExp("(\\.\\s*)"+e.languages.javascript.function.source),lookbehind:!0,alias:["function","property-access"]}}),e.languages.insertBefore("javascript","constant",{"known-class-name":[{pattern:/\b(?:(?:Float(?:32|64)|(?:Int|Uint)(?:8|16|32)|Uint8Clamped)?Array|ArrayBuffer|BigInt|Boolean|DataView|Date|Error|Function|Intl|JSON|(?:Weak)?(?:Map|Set)|Math|Number|Object|Promise|Proxy|Reflect|RegExp|String|Symbol|WebAssembly)\b/,alias:"class-name"},{pattern:/\b(?:[A-Z]\w*)Error\b/,alias:"class-name"}]}),e.languages.insertBefore("javascript","keyword",{imports:{pattern:t(/(\bimport\b\s*)(?:(?:\s*,\s*(?:\*\s*as\s+|\{[^{}]*\}))?|\*\s*as\s+|\{[^{}]*\})(?=\s*\bfrom\b)/.source),lookbehind:!0,inside:e.languages.javascript},exports:{pattern:t(/(\bexport\b\s*)(?:\*(?:\s*as\s+)?(?=\s*\bfrom\b)|\{[^{}]*\})/.source),lookbehind:!0,inside:e.languages.javascript}}),e.languages.javascript.keyword.unshift({pattern:/\b(?:as|default|export|from|import)\b/,alias:"module"},{pattern:/\b(?:await|break|catch|continue|do|else|finally|for|if|return|switch|throw|try|while|yield)\b/,alias:"control-flow"},{pattern:/\bnull\b/,alias:["null","nil"]},{pattern:/\bundefined\b/,alias:"nil"}),e.languages.insertBefore("javascript","operator",{spread:{pattern:/\.{3}/,alias:"operator"},arrow:{pattern:/=>/,alias:"operator"}}),e.languages.insertBefore("javascript","punctuation",{"property-access":{pattern:t(/(\.\s*)#?/.source),lookbehind:!0},"maybe-class-name":{pattern:/(^|[^$\w\xA0-\uFFFF])[A-Z][$\w\xA0-\uFFFF]+/,lookbehind:!0},dom:{pattern:/\b(?:document|(?:local|session)Storage|location|navigator|performance|window)\b/,alias:"variable"},console:{pattern:/\bconsole(?=\s*\.)/,alias:"class-name"}});for(var n=["function","function-variable","method","method-variable","property-access"],r=0;r*\.{3}(?:[^{}]|)*\})/.source;function o(e,t){return e=e.replace(//g,(function(){return n})).replace(//g,(function(){return r})).replace(//g,(function(){return a})),RegExp(e,t)}a=o(a).source,e.languages.jsx=e.languages.extend("markup",t),e.languages.jsx.tag.pattern=o(/<\/?(?:[\w.:-]+(?:+(?:[\w.:$-]+(?:=(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s{'"/>=]+|))?|))**\/?)?>/.source),e.languages.jsx.tag.inside.tag.pattern=/^<\/?[^\s>\/]*/,e.languages.jsx.tag.inside["attr-value"].pattern=/=(?!\{)(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s'">]+)/,e.languages.jsx.tag.inside.tag.inside["class-name"]=/^[A-Z]\w*(?:\.[A-Z]\w*)*$/,e.languages.jsx.tag.inside.comment=t.comment,e.languages.insertBefore("inside","attr-name",{spread:{pattern:o(//.source),inside:e.languages.jsx}},e.languages.jsx.tag),e.languages.insertBefore("inside","special-attr",{script:{pattern:o(/=/.source),alias:"language-javascript",inside:{"script-punctuation":{pattern:/^=(?=\{)/,alias:"punctuation"},rest:e.languages.jsx}}},e.languages.jsx.tag);var i=function(e){return e?"string"==typeof e?e:"string"==typeof e.content?e.content:e.content.map(i).join(""):""},l=function(t){for(var n=[],r=0;r0&&n[n.length-1].tagName===i(a.content[0].content[1])&&n.pop():"/>"===a.content[a.content.length-1].content||n.push({tagName:i(a.content[0].content[1]),openedBraces:0}):n.length>0&&"punctuation"===a.type&&"{"===a.content?n[n.length-1].openedBraces++:n.length>0&&n[n.length-1].openedBraces>0&&"punctuation"===a.type&&"}"===a.content?n[n.length-1].openedBraces--:o=!0),(o||"string"==typeof a)&&n.length>0&&0===n[n.length-1].openedBraces){var s=i(a);r0&&("string"==typeof t[r-1]||"plain-text"===t[r-1].type)&&(s=i(t[r-1])+s,t.splice(r-1,1),r--),t[r]=new e.Token("plain-text",s,null,s)}a.content&&"string"!=typeof a.content&&l(a.content)}};e.hooks.add("after-tokenize",(function(e){"jsx"!==e.language&&"tsx"!==e.language||l(e.tokens)}))}(a),function(e){e.languages.diff={coord:[/^(?:\*{3}|-{3}|\+{3}).*$/m,/^@@.*@@$/m,/^\d.*$/m]};var t={"deleted-sign":"-","deleted-arrow":"<","inserted-sign":"+","inserted-arrow":">",unchanged:" ",diff:"!"};Object.keys(t).forEach((function(n){var r=t[n],a=[];/^\w+$/.test(n)||a.push(/\w+/.exec(n)[0]),"diff"===n&&a.push("bold"),e.languages.diff[n]={pattern:RegExp("^(?:["+r+"].*(?:\r\n?|\n|(?![\\s\\S])))+","m"),alias:a,inside:{line:{pattern:/(.)(?=[\s\S]).*(?:\r\n?|\n)?/,lookbehind:!0},prefix:{pattern:/[\s\S]/,alias:/\w+/.exec(n)[0]}}}})),Object.defineProperty(e.languages.diff,"PREFIXES",{value:t})}(a),a.languages.git={comment:/^#.*/m,deleted:/^[-\u2013].*/m,inserted:/^\+.*/m,string:/("|')(?:\\.|(?!\1)[^\\\r\n])*\1/,command:{pattern:/^.*\$ git .*$/m,inside:{parameter:/\s--?\w+/}},coord:/^@@.*@@$/m,"commit-sha1":/^commit \w{40}$/m},a.languages.go=a.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"|`[^`]*`/,lookbehind:!0,greedy:!0},keyword:/\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,boolean:/\b(?:_|false|iota|nil|true)\b/,number:[/\b0(?:b[01_]+|o[0-7_]+)i?\b/i,/\b0x(?:[a-f\d_]+(?:\.[a-f\d_]*)?|\.[a-f\d_]+)(?:p[+-]?\d+(?:_\d+)*)?i?(?!\w)/i,/(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?[\d_]+)?i?(?!\w)/i],operator:/[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./,builtin:/\b(?:append|bool|byte|cap|close|complex|complex(?:64|128)|copy|delete|error|float(?:32|64)|u?int(?:8|16|32|64)?|imag|len|make|new|panic|print(?:ln)?|real|recover|rune|string|uintptr)\b/}),a.languages.insertBefore("go","string",{char:{pattern:/'(?:\\.|[^'\\\r\n]){0,10}'/,greedy:!0}}),delete a.languages.go["class-name"],function(e){function t(e,t){return"___"+e.toUpperCase()+t+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(n,r,a,o){if(n.language===r){var i=n.tokenStack=[];n.code=n.code.replace(a,(function(e){if("function"==typeof o&&!o(e))return e;for(var a,l=i.length;-1!==n.code.indexOf(a=t(r,l));)++l;return i[l]=e,a})),n.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(n,r){if(n.language===r&&n.tokenStack){n.grammar=e.languages[r];var a=0,o=Object.keys(n.tokenStack);!function i(l){for(var s=0;s=o.length);s++){var u=l[s];if("string"==typeof u||u.content&&"string"==typeof u.content){var c=o[a],d=n.tokenStack[c],f="string"==typeof u?u:u.content,p=t(r,c),m=f.indexOf(p);if(m>-1){++a;var g=f.substring(0,m),h=new e.Token(r,e.tokenize(d,n.grammar),"language-"+r,d),b=f.substring(m+p.length),v=[];g&&v.push.apply(v,i([g])),v.push(h),b&&v.push.apply(v,i([b])),"string"==typeof u?l.splice.apply(l,[s,1].concat(v)):u.content=v}}else u.content&&i(u.content)}return l}(n.tokens)}}}})}(a),function(e){e.languages.handlebars={comment:/\{\{![\s\S]*?\}\}/,delimiter:{pattern:/^\{\{\{?|\}\}\}?$/,alias:"punctuation"},string:/(["'])(?:\\.|(?!\1)[^\\\r\n])*\1/,number:/\b0x[\dA-Fa-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:[Ee][+-]?\d+)?/,boolean:/\b(?:false|true)\b/,block:{pattern:/^(\s*(?:~\s*)?)[#\/]\S+?(?=\s*(?:~\s*)?$|\s)/,lookbehind:!0,alias:"keyword"},brackets:{pattern:/\[[^\]]+\]/,inside:{punctuation:/\[|\]/,variable:/[\s\S]+/}},punctuation:/[!"#%&':()*+,.\/;<=>@\[\\\]^`{|}~]/,variable:/[^!"#%&'()*+,\/;<=>@\[\\\]^`{|}~\s]+/},e.hooks.add("before-tokenize",(function(t){e.languages["markup-templating"].buildPlaceholders(t,"handlebars",/\{\{\{[\s\S]+?\}\}\}|\{\{[\s\S]+?\}\}/g)})),e.hooks.add("after-tokenize",(function(t){e.languages["markup-templating"].tokenizePlaceholders(t,"handlebars")})),e.languages.hbs=e.languages.handlebars}(a),a.languages.json={property:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?=\s*:)/,lookbehind:!0,greedy:!0},string:{pattern:/(^|[^\\])"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,lookbehind:!0,greedy:!0},comment:{pattern:/\/\/.*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},number:/-?\b\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,punctuation:/[{}[\],]/,operator:/:/,boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"}},a.languages.webmanifest=a.languages.json,a.languages.less=a.languages.extend("css",{comment:[/\/\*[\s\S]*?\*\//,{pattern:/(^|[^\\])\/\/.*/,lookbehind:!0}],atrule:{pattern:/@[\w-](?:\((?:[^(){}]|\([^(){}]*\))*\)|[^(){};\s]|\s+(?!\s))*?(?=\s*\{)/,inside:{punctuation:/[:()]/}},selector:{pattern:/(?:@\{[\w-]+\}|[^{};\s@])(?:@\{[\w-]+\}|\((?:[^(){}]|\([^(){}]*\))*\)|[^(){};@\s]|\s+(?!\s))*?(?=\s*\{)/,inside:{variable:/@+[\w-]+/}},property:/(?:@\{[\w-]+\}|[\w-])+(?:\+_?)?(?=\s*:)/,operator:/[+\-*\/]/}),a.languages.insertBefore("less","property",{variable:[{pattern:/@[\w-]+\s*:/,inside:{punctuation:/:/}},/@@?[\w-]+/],"mixin-usage":{pattern:/([{;]\s*)[.#](?!\d)[\w-].*?(?=[(;])/,lookbehind:!0,alias:"function"}}),a.languages.makefile={comment:{pattern:/(^|[^\\])#(?:\\(?:\r\n|[\s\S])|[^\\\r\n])*/,lookbehind:!0},string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"builtin-target":{pattern:/\.[A-Z][^:#=\s]+(?=\s*:(?!=))/,alias:"builtin"},target:{pattern:/^(?:[^:=\s]|[ \t]+(?![\s:]))+(?=\s*:(?!=))/m,alias:"symbol",inside:{variable:/\$+(?:(?!\$)[^(){}:#=\s]+|(?=[({]))/}},variable:/\$+(?:(?!\$)[^(){}:#=\s]+|\([@*%<^+?][DF]\)|(?=[({]))/,keyword:/-include\b|\b(?:define|else|endef|endif|export|ifn?def|ifn?eq|include|override|private|sinclude|undefine|unexport|vpath)\b/,function:{pattern:/(\()(?:abspath|addsuffix|and|basename|call|dir|error|eval|file|filter(?:-out)?|findstring|firstword|flavor|foreach|guile|if|info|join|lastword|load|notdir|or|origin|patsubst|realpath|shell|sort|strip|subst|suffix|value|warning|wildcard|word(?:list|s)?)(?=[ \t])/,lookbehind:!0},operator:/(?:::|[?:+!])?=|[|@]/,punctuation:/[:;(){}]/},a.languages.objectivec=a.languages.extend("c",{string:{pattern:/@?"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,greedy:!0},keyword:/\b(?:asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|in|inline|int|long|register|return|self|short|signed|sizeof|static|struct|super|switch|typedef|typeof|union|unsigned|void|volatile|while)\b|(?:@interface|@end|@implementation|@protocol|@class|@public|@protected|@private|@property|@try|@catch|@finally|@throw|@synthesize|@dynamic|@selector)\b/,operator:/-[->]?|\+\+?|!=?|<>?=?|==?|&&?|\|\|?|[~^%?*\/@]/}),delete a.languages.objectivec["class-name"],a.languages.objc=a.languages.objectivec,a.languages.ocaml={comment:{pattern:/\(\*[\s\S]*?\*\)/,greedy:!0},char:{pattern:/'(?:[^\\\r\n']|\\(?:.|[ox]?[0-9a-f]{1,3}))'/i,greedy:!0},string:[{pattern:/"(?:\\(?:[\s\S]|\r\n)|[^\\\r\n"])*"/,greedy:!0},{pattern:/\{([a-z_]*)\|[\s\S]*?\|\1\}/,greedy:!0}],number:[/\b(?:0b[01][01_]*|0o[0-7][0-7_]*)\b/i,/\b0x[a-f0-9][a-f0-9_]*(?:\.[a-f0-9_]*)?(?:p[+-]?\d[\d_]*)?(?!\w)/i,/\b\d[\d_]*(?:\.[\d_]*)?(?:e[+-]?\d[\d_]*)?(?!\w)/i],directive:{pattern:/\B#\w+/,alias:"property"},label:{pattern:/\B~\w+/,alias:"property"},"type-variable":{pattern:/\B'\w+/,alias:"function"},variant:{pattern:/`\w+/,alias:"symbol"},keyword:/\b(?:as|assert|begin|class|constraint|do|done|downto|else|end|exception|external|for|fun|function|functor|if|in|include|inherit|initializer|lazy|let|match|method|module|mutable|new|nonrec|object|of|open|private|rec|sig|struct|then|to|try|type|val|value|virtual|when|where|while|with)\b/,boolean:/\b(?:false|true)\b/,"operator-like-punctuation":{pattern:/\[[<>|]|[>|]\]|\{<|>\}/,alias:"punctuation"},operator:/\.[.~]|:[=>]|[=<>@^|&+\-*\/$%!?~][!$%&*+\-.\/:<=>?@^|~]*|\b(?:and|asr|land|lor|lsl|lsr|lxor|mod|or)\b/,punctuation:/;;|::|[(){}\[\].,:;#]|\b_\b/},a.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},a.languages.python["string-interpolation"].inside.interpolation.inside.rest=a.languages.python,a.languages.py=a.languages.python,a.languages.reason=a.languages.extend("clike",{string:{pattern:/"(?:\\(?:\r\n|[\s\S])|[^\\\r\n"])*"/,greedy:!0},"class-name":/\b[A-Z]\w*/,keyword:/\b(?:and|as|assert|begin|class|constraint|do|done|downto|else|end|exception|external|for|fun|function|functor|if|in|include|inherit|initializer|lazy|let|method|module|mutable|new|nonrec|object|of|open|or|private|rec|sig|struct|switch|then|to|try|type|val|virtual|when|while|with)\b/,operator:/\.{3}|:[:=]|\|>|->|=(?:==?|>)?|<=?|>=?|[|^?'#!~`]|[+\-*\/]\.?|\b(?:asr|land|lor|lsl|lsr|lxor|mod)\b/}),a.languages.insertBefore("reason","class-name",{char:{pattern:/'(?:\\x[\da-f]{2}|\\o[0-3][0-7][0-7]|\\\d{3}|\\.|[^'\\\r\n])'/,greedy:!0},constructor:/\b[A-Z]\w*\b(?!\s*\.)/,label:{pattern:/\b[a-z]\w*(?=::)/,alias:"symbol"}}),delete a.languages.reason.function,function(e){e.languages.sass=e.languages.extend("css",{comment:{pattern:/^([ \t]*)\/[\/*].*(?:(?:\r?\n|\r)\1[ \t].+)*/m,lookbehind:!0,greedy:!0}}),e.languages.insertBefore("sass","atrule",{"atrule-line":{pattern:/^(?:[ \t]*)[@+=].+/m,greedy:!0,inside:{atrule:/(?:@[\w-]+|[+=])/}}}),delete e.languages.sass.atrule;var t=/\$[-\w]+|#\{\$[-\w]+\}/,n=[/[+*\/%]|[=!]=|<=?|>=?|\b(?:and|not|or)\b/,{pattern:/(\s)-(?=\s)/,lookbehind:!0}];e.languages.insertBefore("sass","property",{"variable-line":{pattern:/^[ \t]*\$.+/m,greedy:!0,inside:{punctuation:/:/,variable:t,operator:n}},"property-line":{pattern:/^[ \t]*(?:[^:\s]+ *:.*|:[^:\s].*)/m,greedy:!0,inside:{property:[/[^:\s]+(?=\s*:)/,{pattern:/(:)[^:\s]+/,lookbehind:!0}],punctuation:/:/,variable:t,operator:n,important:e.languages.sass.important}}}),delete e.languages.sass.property,delete e.languages.sass.important,e.languages.insertBefore("sass","punctuation",{selector:{pattern:/^([ \t]*)\S(?:,[^,\r\n]+|[^,\r\n]*)(?:,[^,\r\n]+)*(?:,(?:\r?\n|\r)\1[ \t]+\S(?:,[^,\r\n]+|[^,\r\n]*)(?:,[^,\r\n]+)*)*/m,lookbehind:!0,greedy:!0}})}(a),a.languages.scss=a.languages.extend("css",{comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|\/\/.*)/,lookbehind:!0},atrule:{pattern:/@[\w-](?:\([^()]+\)|[^()\s]|\s+(?!\s))*?(?=\s+[{;])/,inside:{rule:/@[\w-]+/}},url:/(?:[-a-z]+-)?url(?=\()/i,selector:{pattern:/(?=\S)[^@;{}()]?(?:[^@;{}()\s]|\s+(?!\s)|#\{\$[-\w]+\})+(?=\s*\{(?:\}|\s|[^}][^:{}]*[:{][^}]))/,inside:{parent:{pattern:/&/,alias:"important"},placeholder:/%[-\w]+/,variable:/\$[-\w]+|#\{\$[-\w]+\}/}},property:{pattern:/(?:[-\w]|\$[-\w]|#\{\$[-\w]+\})+(?=\s*:)/,inside:{variable:/\$[-\w]+|#\{\$[-\w]+\}/}}}),a.languages.insertBefore("scss","atrule",{keyword:[/@(?:content|debug|each|else(?: if)?|extend|for|forward|function|if|import|include|mixin|return|use|warn|while)\b/i,{pattern:/( )(?:from|through)(?= )/,lookbehind:!0}]}),a.languages.insertBefore("scss","important",{variable:/\$[-\w]+|#\{\$[-\w]+\}/}),a.languages.insertBefore("scss","function",{"module-modifier":{pattern:/\b(?:as|hide|show|with)\b/i,alias:"keyword"},placeholder:{pattern:/%[-\w]+/,alias:"selector"},statement:{pattern:/\B!(?:default|optional)\b/i,alias:"keyword"},boolean:/\b(?:false|true)\b/,null:{pattern:/\bnull\b/,alias:"keyword"},operator:{pattern:/(\s)(?:[-+*\/%]|[=!]=|<=?|>=?|and|not|or)(?=\s)/,lookbehind:!0}}),a.languages.scss.atrule.inside.rest=a.languages.scss,function(e){var t={pattern:/(\b\d+)(?:%|[a-z]+)/,lookbehind:!0},n={pattern:/(^|[^\w.-])-?(?:\d+(?:\.\d+)?|\.\d+)/,lookbehind:!0},r={comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|\/\/.*)/,lookbehind:!0},url:{pattern:/\burl\((["']?).*?\1\)/i,greedy:!0},string:{pattern:/("|')(?:(?!\1)[^\\\r\n]|\\(?:\r\n|[\s\S]))*\1/,greedy:!0},interpolation:null,func:null,important:/\B!(?:important|optional)\b/i,keyword:{pattern:/(^|\s+)(?:(?:else|for|if|return|unless)(?=\s|$)|@[\w-]+)/,lookbehind:!0},hexcode:/#[\da-f]{3,6}/i,color:[/\b(?:AliceBlue|AntiqueWhite|Aqua|Aquamarine|Azure|Beige|Bisque|Black|BlanchedAlmond|Blue|BlueViolet|Brown|BurlyWood|CadetBlue|Chartreuse|Chocolate|Coral|CornflowerBlue|Cornsilk|Crimson|Cyan|DarkBlue|DarkCyan|DarkGoldenRod|DarkGr[ae]y|DarkGreen|DarkKhaki|DarkMagenta|DarkOliveGreen|DarkOrange|DarkOrchid|DarkRed|DarkSalmon|DarkSeaGreen|DarkSlateBlue|DarkSlateGr[ae]y|DarkTurquoise|DarkViolet|DeepPink|DeepSkyBlue|DimGr[ae]y|DodgerBlue|FireBrick|FloralWhite|ForestGreen|Fuchsia|Gainsboro|GhostWhite|Gold|GoldenRod|Gr[ae]y|Green|GreenYellow|HoneyDew|HotPink|IndianRed|Indigo|Ivory|Khaki|Lavender|LavenderBlush|LawnGreen|LemonChiffon|LightBlue|LightCoral|LightCyan|LightGoldenRodYellow|LightGr[ae]y|LightGreen|LightPink|LightSalmon|LightSeaGreen|LightSkyBlue|LightSlateGr[ae]y|LightSteelBlue|LightYellow|Lime|LimeGreen|Linen|Magenta|Maroon|MediumAquaMarine|MediumBlue|MediumOrchid|MediumPurple|MediumSeaGreen|MediumSlateBlue|MediumSpringGreen|MediumTurquoise|MediumVioletRed|MidnightBlue|MintCream|MistyRose|Moccasin|NavajoWhite|Navy|OldLace|Olive|OliveDrab|Orange|OrangeRed|Orchid|PaleGoldenRod|PaleGreen|PaleTurquoise|PaleVioletRed|PapayaWhip|PeachPuff|Peru|Pink|Plum|PowderBlue|Purple|Red|RosyBrown|RoyalBlue|SaddleBrown|Salmon|SandyBrown|SeaGreen|SeaShell|Sienna|Silver|SkyBlue|SlateBlue|SlateGr[ae]y|Snow|SpringGreen|SteelBlue|Tan|Teal|Thistle|Tomato|Transparent|Turquoise|Violet|Wheat|White|WhiteSmoke|Yellow|YellowGreen)\b/i,{pattern:/\b(?:hsl|rgb)\(\s*\d{1,3}\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*\)\B|\b(?:hsl|rgb)a\(\s*\d{1,3}\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*,\s*(?:0|0?\.\d+|1)\s*\)\B/i,inside:{unit:t,number:n,function:/[\w-]+(?=\()/,punctuation:/[(),]/}}],entity:/\\[\da-f]{1,8}/i,unit:t,boolean:/\b(?:false|true)\b/,operator:[/~|[+!\/%<>?=]=?|[-:]=|\*[*=]?|\.{2,3}|&&|\|\||\B-\B|\b(?:and|in|is(?: a| defined| not|nt)?|not|or)\b/],number:n,punctuation:/[{}()\[\];:,]/};r.interpolation={pattern:/\{[^\r\n}:]+\}/,alias:"variable",inside:{delimiter:{pattern:/^\{|\}$/,alias:"punctuation"},rest:r}},r.func={pattern:/[\w-]+\([^)]*\).*/,inside:{function:/^[^(]+/,rest:r}},e.languages.stylus={"atrule-declaration":{pattern:/(^[ \t]*)@.+/m,lookbehind:!0,inside:{atrule:/^@[\w-]+/,rest:r}},"variable-declaration":{pattern:/(^[ \t]*)[\w$-]+\s*.?=[ \t]*(?:\{[^{}]*\}|\S.*|$)/m,lookbehind:!0,inside:{variable:/^\S+/,rest:r}},statement:{pattern:/(^[ \t]*)(?:else|for|if|return|unless)[ \t].+/m,lookbehind:!0,inside:{keyword:/^\S+/,rest:r}},"property-declaration":{pattern:/((?:^|\{)([ \t]*))(?:[\w-]|\{[^}\r\n]+\})+(?:\s*:\s*|[ \t]+)(?!\s)[^{\r\n]*(?:;|[^{\r\n,]$(?!(?:\r?\n|\r)(?:\{|\2[ \t])))/m,lookbehind:!0,inside:{property:{pattern:/^[^\s:]+/,inside:{interpolation:r.interpolation}},rest:r}},selector:{pattern:/(^[ \t]*)(?:(?=\S)(?:[^{}\r\n:()]|::?[\w-]+(?:\([^)\r\n]*\)|(?![\w-]))|\{[^}\r\n]+\})+)(?:(?:\r?\n|\r)(?:\1(?:(?=\S)(?:[^{}\r\n:()]|::?[\w-]+(?:\([^)\r\n]*\)|(?![\w-]))|\{[^}\r\n]+\})+)))*(?:,$|\{|(?=(?:\r?\n|\r)(?:\{|\1[ \t])))/m,lookbehind:!0,inside:{interpolation:r.interpolation,comment:r.comment,punctuation:/[{},]/}},func:r.func,string:r.string,comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|\/\/.*)/,lookbehind:!0,greedy:!0},interpolation:r.interpolation,punctuation:/[{}()\[\];:.]/}}(a),function(e){var t=e.util.clone(e.languages.typescript);e.languages.tsx=e.languages.extend("jsx",t),delete e.languages.tsx.parameter,delete e.languages.tsx["literal-property"];var n=e.languages.tsx.tag;n.pattern=RegExp(/(^|[^\w$]|(?=<\/))/.source+"(?:"+n.pattern.source+")",n.pattern.flags),n.lookbehind=!0}(a),a.languages.wasm={comment:[/\(;[\s\S]*?;\)/,{pattern:/;;.*/,greedy:!0}],string:{pattern:/"(?:\\[\s\S]|[^"\\])*"/,greedy:!0},keyword:[{pattern:/\b(?:align|offset)=/,inside:{operator:/=/}},{pattern:/\b(?:(?:f32|f64|i32|i64)(?:\.(?:abs|add|and|ceil|clz|const|convert_[su]\/i(?:32|64)|copysign|ctz|demote\/f64|div(?:_[su])?|eqz?|extend_[su]\/i32|floor|ge(?:_[su])?|gt(?:_[su])?|le(?:_[su])?|load(?:(?:8|16|32)_[su])?|lt(?:_[su])?|max|min|mul|neg?|nearest|or|popcnt|promote\/f32|reinterpret\/[fi](?:32|64)|rem_[su]|rot[lr]|shl|shr_[su]|sqrt|store(?:8|16|32)?|sub|trunc(?:_[su]\/f(?:32|64))?|wrap\/i64|xor))?|memory\.(?:grow|size))\b/,inside:{punctuation:/\./}},/\b(?:anyfunc|block|br(?:_if|_table)?|call(?:_indirect)?|data|drop|elem|else|end|export|func|get_(?:global|local)|global|if|import|local|loop|memory|module|mut|nop|offset|param|result|return|select|set_(?:global|local)|start|table|tee_local|then|type|unreachable)\b/],variable:/\$[\w!#$%&'*+\-./:<=>?@\\^`|~]+/,number:/[+-]?\b(?:\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|0x[\da-fA-F](?:_?[\da-fA-F])*(?:\.[\da-fA-F](?:_?[\da-fA-D])*)?(?:[pP][+-]?\d(?:_?\d)*)?)\b|\binf\b|\bnan(?::0x[\da-fA-F](?:_?[\da-fA-D])*)?\b/,punctuation:/[()]/};const o=a},9901:e=>{e.exports&&(e.exports={core:{meta:{path:"components/prism-core.js",option:"mandatory"},core:"Core"},themes:{meta:{path:"themes/{id}.css",link:"index.html?theme={id}",exclusive:!0},prism:{title:"Default",option:"default"},"prism-dark":"Dark","prism-funky":"Funky","prism-okaidia":{title:"Okaidia",owner:"ocodia"},"prism-twilight":{title:"Twilight",owner:"remybach"},"prism-coy":{title:"Coy",owner:"tshedor"},"prism-solarizedlight":{title:"Solarized Light",owner:"hectormatos2011 "},"prism-tomorrow":{title:"Tomorrow Night",owner:"Rosey"}},languages:{meta:{path:"components/prism-{id}",noCSS:!0,examplesPath:"examples/prism-{id}",addCheckAll:!0},markup:{title:"Markup",alias:["html","xml","svg","mathml","ssml","atom","rss"],aliasTitles:{html:"HTML",xml:"XML",svg:"SVG",mathml:"MathML",ssml:"SSML",atom:"Atom",rss:"RSS"},option:"default"},css:{title:"CSS",option:"default",modify:"markup"},clike:{title:"C-like",option:"default"},javascript:{title:"JavaScript",require:"clike",modify:"markup",optional:"regex",alias:"js",option:"default"},abap:{title:"ABAP",owner:"dellagustin"},abnf:{title:"ABNF",owner:"RunDevelopment"},actionscript:{title:"ActionScript",require:"javascript",modify:"markup",owner:"Golmote"},ada:{title:"Ada",owner:"Lucretia"},agda:{title:"Agda",owner:"xy-ren"},al:{title:"AL",owner:"RunDevelopment"},antlr4:{title:"ANTLR4",alias:"g4",owner:"RunDevelopment"},apacheconf:{title:"Apache Configuration",owner:"GuiTeK"},apex:{title:"Apex",require:["clike","sql"],owner:"RunDevelopment"},apl:{title:"APL",owner:"ngn"},applescript:{title:"AppleScript",owner:"Golmote"},aql:{title:"AQL",owner:"RunDevelopment"},arduino:{title:"Arduino",require:"cpp",alias:"ino",owner:"dkern"},arff:{title:"ARFF",owner:"Golmote"},armasm:{title:"ARM Assembly",alias:"arm-asm",owner:"RunDevelopment"},arturo:{title:"Arturo",alias:"art",optional:["bash","css","javascript","markup","markdown","sql"],owner:"drkameleon"},asciidoc:{alias:"adoc",title:"AsciiDoc",owner:"Golmote"},aspnet:{title:"ASP.NET (C#)",require:["markup","csharp"],owner:"nauzilus"},asm6502:{title:"6502 Assembly",owner:"kzurawel"},asmatmel:{title:"Atmel AVR Assembly",owner:"cerkit"},autohotkey:{title:"AutoHotkey",owner:"aviaryan"},autoit:{title:"AutoIt",owner:"Golmote"},avisynth:{title:"AviSynth",alias:"avs",owner:"Zinfidel"},"avro-idl":{title:"Avro IDL",alias:"avdl",owner:"RunDevelopment"},awk:{title:"AWK",alias:"gawk",aliasTitles:{gawk:"GAWK"},owner:"RunDevelopment"},bash:{title:"Bash",alias:["sh","shell"],aliasTitles:{sh:"Shell",shell:"Shell"},owner:"zeitgeist87"},basic:{title:"BASIC",owner:"Golmote"},batch:{title:"Batch",owner:"Golmote"},bbcode:{title:"BBcode",alias:"shortcode",aliasTitles:{shortcode:"Shortcode"},owner:"RunDevelopment"},bbj:{title:"BBj",owner:"hyyan"},bicep:{title:"Bicep",owner:"johnnyreilly"},birb:{title:"Birb",require:"clike",owner:"Calamity210"},bison:{title:"Bison",require:"c",owner:"Golmote"},bnf:{title:"BNF",alias:"rbnf",aliasTitles:{rbnf:"RBNF"},owner:"RunDevelopment"},bqn:{title:"BQN",owner:"yewscion"},brainfuck:{title:"Brainfuck",owner:"Golmote"},brightscript:{title:"BrightScript",owner:"RunDevelopment"},bro:{title:"Bro",owner:"wayward710"},bsl:{title:"BSL (1C:Enterprise)",alias:"oscript",aliasTitles:{oscript:"OneScript"},owner:"Diversus23"},c:{title:"C",require:"clike",owner:"zeitgeist87"},csharp:{title:"C#",require:"clike",alias:["cs","dotnet"],owner:"mvalipour"},cpp:{title:"C++",require:"c",owner:"zeitgeist87"},cfscript:{title:"CFScript",require:"clike",alias:"cfc",owner:"mjclemente"},chaiscript:{title:"ChaiScript",require:["clike","cpp"],owner:"RunDevelopment"},cil:{title:"CIL",owner:"sbrl"},cilkc:{title:"Cilk/C",require:"c",alias:"cilk-c",owner:"OpenCilk"},cilkcpp:{title:"Cilk/C++",require:"cpp",alias:["cilk-cpp","cilk"],owner:"OpenCilk"},clojure:{title:"Clojure",owner:"troglotit"},cmake:{title:"CMake",owner:"mjrogozinski"},cobol:{title:"COBOL",owner:"RunDevelopment"},coffeescript:{title:"CoffeeScript",require:"javascript",alias:"coffee",owner:"R-osey"},concurnas:{title:"Concurnas",alias:"conc",owner:"jasontatton"},csp:{title:"Content-Security-Policy",owner:"ScottHelme"},cooklang:{title:"Cooklang",owner:"ahue"},coq:{title:"Coq",owner:"RunDevelopment"},crystal:{title:"Crystal",require:"ruby",owner:"MakeNowJust"},"css-extras":{title:"CSS Extras",require:"css",modify:"css",owner:"milesj"},csv:{title:"CSV",owner:"RunDevelopment"},cue:{title:"CUE",owner:"RunDevelopment"},cypher:{title:"Cypher",owner:"RunDevelopment"},d:{title:"D",require:"clike",owner:"Golmote"},dart:{title:"Dart",require:"clike",owner:"Golmote"},dataweave:{title:"DataWeave",owner:"machaval"},dax:{title:"DAX",owner:"peterbud"},dhall:{title:"Dhall",owner:"RunDevelopment"},diff:{title:"Diff",owner:"uranusjr"},django:{title:"Django/Jinja2",require:"markup-templating",alias:"jinja2",owner:"romanvm"},"dns-zone-file":{title:"DNS zone file",owner:"RunDevelopment",alias:"dns-zone"},docker:{title:"Docker",alias:"dockerfile",owner:"JustinBeckwith"},dot:{title:"DOT (Graphviz)",alias:"gv",optional:"markup",owner:"RunDevelopment"},ebnf:{title:"EBNF",owner:"RunDevelopment"},editorconfig:{title:"EditorConfig",owner:"osipxd"},eiffel:{title:"Eiffel",owner:"Conaclos"},ejs:{title:"EJS",require:["javascript","markup-templating"],owner:"RunDevelopment",alias:"eta",aliasTitles:{eta:"Eta"}},elixir:{title:"Elixir",owner:"Golmote"},elm:{title:"Elm",owner:"zwilias"},etlua:{title:"Embedded Lua templating",require:["lua","markup-templating"],owner:"RunDevelopment"},erb:{title:"ERB",require:["ruby","markup-templating"],owner:"Golmote"},erlang:{title:"Erlang",owner:"Golmote"},"excel-formula":{title:"Excel Formula",alias:["xlsx","xls"],owner:"RunDevelopment"},fsharp:{title:"F#",require:"clike",owner:"simonreynolds7"},factor:{title:"Factor",owner:"catb0t"},false:{title:"False",owner:"edukisto"},"firestore-security-rules":{title:"Firestore security rules",require:"clike",owner:"RunDevelopment"},flow:{title:"Flow",require:"javascript",owner:"Golmote"},fortran:{title:"Fortran",owner:"Golmote"},ftl:{title:"FreeMarker Template Language",require:"markup-templating",owner:"RunDevelopment"},gml:{title:"GameMaker Language",alias:"gamemakerlanguage",require:"clike",owner:"LiarOnce"},gap:{title:"GAP (CAS)",owner:"RunDevelopment"},gcode:{title:"G-code",owner:"RunDevelopment"},gdscript:{title:"GDScript",owner:"RunDevelopment"},gedcom:{title:"GEDCOM",owner:"Golmote"},gettext:{title:"gettext",alias:"po",owner:"RunDevelopment"},gherkin:{title:"Gherkin",owner:"hason"},git:{title:"Git",owner:"lgiraudel"},glsl:{title:"GLSL",require:"c",owner:"Golmote"},gn:{title:"GN",alias:"gni",owner:"RunDevelopment"},"linker-script":{title:"GNU Linker Script",alias:"ld",owner:"RunDevelopment"},go:{title:"Go",require:"clike",owner:"arnehormann"},"go-module":{title:"Go module",alias:"go-mod",owner:"RunDevelopment"},gradle:{title:"Gradle",require:"clike",owner:"zeabdelkhalek-badido18"},graphql:{title:"GraphQL",optional:"markdown",owner:"Golmote"},groovy:{title:"Groovy",require:"clike",owner:"robfletcher"},haml:{title:"Haml",require:"ruby",optional:["css","css-extras","coffeescript","erb","javascript","less","markdown","scss","textile"],owner:"Golmote"},handlebars:{title:"Handlebars",require:"markup-templating",alias:["hbs","mustache"],aliasTitles:{mustache:"Mustache"},owner:"Golmote"},haskell:{title:"Haskell",alias:"hs",owner:"bholst"},haxe:{title:"Haxe",require:"clike",optional:"regex",owner:"Golmote"},hcl:{title:"HCL",owner:"outsideris"},hlsl:{title:"HLSL",require:"c",owner:"RunDevelopment"},hoon:{title:"Hoon",owner:"matildepark"},http:{title:"HTTP",optional:["csp","css","hpkp","hsts","javascript","json","markup","uri"],owner:"danielgtaylor"},hpkp:{title:"HTTP Public-Key-Pins",owner:"ScottHelme"},hsts:{title:"HTTP Strict-Transport-Security",owner:"ScottHelme"},ichigojam:{title:"IchigoJam",owner:"BlueCocoa"},icon:{title:"Icon",owner:"Golmote"},"icu-message-format":{title:"ICU Message Format",owner:"RunDevelopment"},idris:{title:"Idris",alias:"idr",owner:"KeenS",require:"haskell"},ignore:{title:".ignore",owner:"osipxd",alias:["gitignore","hgignore","npmignore"],aliasTitles:{gitignore:".gitignore",hgignore:".hgignore",npmignore:".npmignore"}},inform7:{title:"Inform 7",owner:"Golmote"},ini:{title:"Ini",owner:"aviaryan"},io:{title:"Io",owner:"AlesTsurko"},j:{title:"J",owner:"Golmote"},java:{title:"Java",require:"clike",owner:"sherblot"},javadoc:{title:"JavaDoc",require:["markup","java","javadoclike"],modify:"java",optional:"scala",owner:"RunDevelopment"},javadoclike:{title:"JavaDoc-like",modify:["java","javascript","php"],owner:"RunDevelopment"},javastacktrace:{title:"Java stack trace",owner:"RunDevelopment"},jexl:{title:"Jexl",owner:"czosel"},jolie:{title:"Jolie",require:"clike",owner:"thesave"},jq:{title:"JQ",owner:"RunDevelopment"},jsdoc:{title:"JSDoc",require:["javascript","javadoclike","typescript"],modify:"javascript",optional:["actionscript","coffeescript"],owner:"RunDevelopment"},"js-extras":{title:"JS Extras",require:"javascript",modify:"javascript",optional:["actionscript","coffeescript","flow","n4js","typescript"],owner:"RunDevelopment"},json:{title:"JSON",alias:"webmanifest",aliasTitles:{webmanifest:"Web App Manifest"},owner:"CupOfTea696"},json5:{title:"JSON5",require:"json",owner:"RunDevelopment"},jsonp:{title:"JSONP",require:"json",owner:"RunDevelopment"},jsstacktrace:{title:"JS stack trace",owner:"sbrl"},"js-templates":{title:"JS Templates",require:"javascript",modify:"javascript",optional:["css","css-extras","graphql","markdown","markup","sql"],owner:"RunDevelopment"},julia:{title:"Julia",owner:"cdagnino"},keepalived:{title:"Keepalived Configure",owner:"dev-itsheng"},keyman:{title:"Keyman",owner:"mcdurdin"},kotlin:{title:"Kotlin",alias:["kt","kts"],aliasTitles:{kts:"Kotlin Script"},require:"clike",owner:"Golmote"},kumir:{title:"KuMir (\u041a\u0443\u041c\u0438\u0440)",alias:"kum",owner:"edukisto"},kusto:{title:"Kusto",owner:"RunDevelopment"},latex:{title:"LaTeX",alias:["tex","context"],aliasTitles:{tex:"TeX",context:"ConTeXt"},owner:"japborst"},latte:{title:"Latte",require:["clike","markup-templating","php"],owner:"nette"},less:{title:"Less",require:"css",optional:"css-extras",owner:"Golmote"},lilypond:{title:"LilyPond",require:"scheme",alias:"ly",owner:"RunDevelopment"},liquid:{title:"Liquid",require:"markup-templating",owner:"cinhtau"},lisp:{title:"Lisp",alias:["emacs","elisp","emacs-lisp"],owner:"JuanCaicedo"},livescript:{title:"LiveScript",owner:"Golmote"},llvm:{title:"LLVM IR",owner:"porglezomp"},log:{title:"Log file",optional:"javastacktrace",owner:"RunDevelopment"},lolcode:{title:"LOLCODE",owner:"Golmote"},lua:{title:"Lua",owner:"Golmote"},magma:{title:"Magma (CAS)",owner:"RunDevelopment"},makefile:{title:"Makefile",owner:"Golmote"},markdown:{title:"Markdown",require:"markup",optional:"yaml",alias:"md",owner:"Golmote"},"markup-templating":{title:"Markup templating",require:"markup",owner:"Golmote"},mata:{title:"Mata",owner:"RunDevelopment"},matlab:{title:"MATLAB",owner:"Golmote"},maxscript:{title:"MAXScript",owner:"RunDevelopment"},mel:{title:"MEL",owner:"Golmote"},mermaid:{title:"Mermaid",owner:"RunDevelopment"},metafont:{title:"METAFONT",owner:"LaeriExNihilo"},mizar:{title:"Mizar",owner:"Golmote"},mongodb:{title:"MongoDB",owner:"airs0urce",require:"javascript"},monkey:{title:"Monkey",owner:"Golmote"},moonscript:{title:"MoonScript",alias:"moon",owner:"RunDevelopment"},n1ql:{title:"N1QL",owner:"TMWilds"},n4js:{title:"N4JS",require:"javascript",optional:"jsdoc",alias:"n4jsd",owner:"bsmith-n4"},"nand2tetris-hdl":{title:"Nand To Tetris HDL",owner:"stephanmax"},naniscript:{title:"Naninovel Script",owner:"Elringus",alias:"nani"},nasm:{title:"NASM",owner:"rbmj"},neon:{title:"NEON",owner:"nette"},nevod:{title:"Nevod",owner:"nezaboodka"},nginx:{title:"nginx",owner:"volado"},nim:{title:"Nim",owner:"Golmote"},nix:{title:"Nix",owner:"Golmote"},nsis:{title:"NSIS",owner:"idleberg"},objectivec:{title:"Objective-C",require:"c",alias:"objc",owner:"uranusjr"},ocaml:{title:"OCaml",owner:"Golmote"},odin:{title:"Odin",owner:"edukisto"},opencl:{title:"OpenCL",require:"c",modify:["c","cpp"],owner:"Milania1"},openqasm:{title:"OpenQasm",alias:"qasm",owner:"RunDevelopment"},oz:{title:"Oz",owner:"Golmote"},parigp:{title:"PARI/GP",owner:"Golmote"},parser:{title:"Parser",require:"markup",owner:"Golmote"},pascal:{title:"Pascal",alias:"objectpascal",aliasTitles:{objectpascal:"Object Pascal"},owner:"Golmote"},pascaligo:{title:"Pascaligo",owner:"DefinitelyNotAGoat"},psl:{title:"PATROL Scripting Language",owner:"bertysentry"},pcaxis:{title:"PC-Axis",alias:"px",owner:"RunDevelopment"},peoplecode:{title:"PeopleCode",alias:"pcode",owner:"RunDevelopment"},perl:{title:"Perl",owner:"Golmote"},php:{title:"PHP",require:"markup-templating",owner:"milesj"},phpdoc:{title:"PHPDoc",require:["php","javadoclike"],modify:"php",owner:"RunDevelopment"},"php-extras":{title:"PHP Extras",require:"php",modify:"php",owner:"milesj"},"plant-uml":{title:"PlantUML",alias:"plantuml",owner:"RunDevelopment"},plsql:{title:"PL/SQL",require:"sql",owner:"Golmote"},powerquery:{title:"PowerQuery",alias:["pq","mscript"],owner:"peterbud"},powershell:{title:"PowerShell",owner:"nauzilus"},processing:{title:"Processing",require:"clike",owner:"Golmote"},prolog:{title:"Prolog",owner:"Golmote"},promql:{title:"PromQL",owner:"arendjr"},properties:{title:".properties",owner:"Golmote"},protobuf:{title:"Protocol Buffers",require:"clike",owner:"just-boris"},pug:{title:"Pug",require:["markup","javascript"],optional:["coffeescript","ejs","handlebars","less","livescript","markdown","scss","stylus","twig"],owner:"Golmote"},puppet:{title:"Puppet",owner:"Golmote"},pure:{title:"Pure",optional:["c","cpp","fortran"],owner:"Golmote"},purebasic:{title:"PureBasic",require:"clike",alias:"pbfasm",owner:"HeX0R101"},purescript:{title:"PureScript",require:"haskell",alias:"purs",owner:"sriharshachilakapati"},python:{title:"Python",alias:"py",owner:"multipetros"},qsharp:{title:"Q#",require:"clike",alias:"qs",owner:"fedonman"},q:{title:"Q (kdb+ database)",owner:"Golmote"},qml:{title:"QML",require:"javascript",owner:"RunDevelopment"},qore:{title:"Qore",require:"clike",owner:"temnroegg"},r:{title:"R",owner:"Golmote"},racket:{title:"Racket",require:"scheme",alias:"rkt",owner:"RunDevelopment"},cshtml:{title:"Razor C#",alias:"razor",require:["markup","csharp"],optional:["css","css-extras","javascript","js-extras"],owner:"RunDevelopment"},jsx:{title:"React JSX",require:["markup","javascript"],optional:["jsdoc","js-extras","js-templates"],owner:"vkbansal"},tsx:{title:"React TSX",require:["jsx","typescript"]},reason:{title:"Reason",require:"clike",owner:"Golmote"},regex:{title:"Regex",owner:"RunDevelopment"},rego:{title:"Rego",owner:"JordanSh"},renpy:{title:"Ren'py",alias:"rpy",owner:"HyuchiaDiego"},rescript:{title:"ReScript",alias:"res",owner:"vmarcosp"},rest:{title:"reST (reStructuredText)",owner:"Golmote"},rip:{title:"Rip",owner:"ravinggenius"},roboconf:{title:"Roboconf",owner:"Golmote"},robotframework:{title:"Robot Framework",alias:"robot",owner:"RunDevelopment"},ruby:{title:"Ruby",require:"clike",alias:"rb",owner:"samflores"},rust:{title:"Rust",owner:"Golmote"},sas:{title:"SAS",optional:["groovy","lua","sql"],owner:"Golmote"},sass:{title:"Sass (Sass)",require:"css",optional:"css-extras",owner:"Golmote"},scss:{title:"Sass (SCSS)",require:"css",optional:"css-extras",owner:"MoOx"},scala:{title:"Scala",require:"java",owner:"jozic"},scheme:{title:"Scheme",owner:"bacchus123"},"shell-session":{title:"Shell session",require:"bash",alias:["sh-session","shellsession"],owner:"RunDevelopment"},smali:{title:"Smali",owner:"RunDevelopment"},smalltalk:{title:"Smalltalk",owner:"Golmote"},smarty:{title:"Smarty",require:"markup-templating",optional:"php",owner:"Golmote"},sml:{title:"SML",alias:"smlnj",aliasTitles:{smlnj:"SML/NJ"},owner:"RunDevelopment"},solidity:{title:"Solidity (Ethereum)",alias:"sol",require:"clike",owner:"glachaud"},"solution-file":{title:"Solution file",alias:"sln",owner:"RunDevelopment"},soy:{title:"Soy (Closure Template)",require:"markup-templating",owner:"Golmote"},sparql:{title:"SPARQL",require:"turtle",owner:"Triply-Dev",alias:"rq"},"splunk-spl":{title:"Splunk SPL",owner:"RunDevelopment"},sqf:{title:"SQF: Status Quo Function (Arma 3)",require:"clike",owner:"RunDevelopment"},sql:{title:"SQL",owner:"multipetros"},squirrel:{title:"Squirrel",require:"clike",owner:"RunDevelopment"},stan:{title:"Stan",owner:"RunDevelopment"},stata:{title:"Stata Ado",require:["mata","java","python"],owner:"RunDevelopment"},iecst:{title:"Structured Text (IEC 61131-3)",owner:"serhioromano"},stylus:{title:"Stylus",owner:"vkbansal"},supercollider:{title:"SuperCollider",alias:"sclang",owner:"RunDevelopment"},swift:{title:"Swift",owner:"chrischares"},systemd:{title:"Systemd configuration file",owner:"RunDevelopment"},"t4-templating":{title:"T4 templating",owner:"RunDevelopment"},"t4-cs":{title:"T4 Text Templates (C#)",require:["t4-templating","csharp"],alias:"t4",owner:"RunDevelopment"},"t4-vb":{title:"T4 Text Templates (VB)",require:["t4-templating","vbnet"],owner:"RunDevelopment"},tap:{title:"TAP",owner:"isaacs",require:"yaml"},tcl:{title:"Tcl",owner:"PeterChaplin"},tt2:{title:"Template Toolkit 2",require:["clike","markup-templating"],owner:"gflohr"},textile:{title:"Textile",require:"markup",optional:"css",owner:"Golmote"},toml:{title:"TOML",owner:"RunDevelopment"},tremor:{title:"Tremor",alias:["trickle","troy"],owner:"darach",aliasTitles:{trickle:"trickle",troy:"troy"}},turtle:{title:"Turtle",alias:"trig",aliasTitles:{trig:"TriG"},owner:"jakubklimek"},twig:{title:"Twig",require:"markup-templating",owner:"brandonkelly"},typescript:{title:"TypeScript",require:"javascript",optional:"js-templates",alias:"ts",owner:"vkbansal"},typoscript:{title:"TypoScript",alias:"tsconfig",aliasTitles:{tsconfig:"TSConfig"},owner:"dkern"},unrealscript:{title:"UnrealScript",alias:["uscript","uc"],owner:"RunDevelopment"},uorazor:{title:"UO Razor Script",owner:"jaseowns"},uri:{title:"URI",alias:"url",aliasTitles:{url:"URL"},owner:"RunDevelopment"},v:{title:"V",require:"clike",owner:"taggon"},vala:{title:"Vala",require:"clike",optional:"regex",owner:"TemplarVolk"},vbnet:{title:"VB.Net",require:"basic",owner:"Bigsby"},velocity:{title:"Velocity",require:"markup",owner:"Golmote"},verilog:{title:"Verilog",owner:"a-rey"},vhdl:{title:"VHDL",owner:"a-rey"},vim:{title:"vim",owner:"westonganger"},"visual-basic":{title:"Visual Basic",alias:["vb","vba"],aliasTitles:{vba:"VBA"},owner:"Golmote"},warpscript:{title:"WarpScript",owner:"RunDevelopment"},wasm:{title:"WebAssembly",owner:"Golmote"},"web-idl":{title:"Web IDL",alias:"webidl",owner:"RunDevelopment"},wgsl:{title:"WGSL",owner:"Dr4gonthree"},wiki:{title:"Wiki markup",require:"markup",owner:"Golmote"},wolfram:{title:"Wolfram language",alias:["mathematica","nb","wl"],aliasTitles:{mathematica:"Mathematica",nb:"Mathematica Notebook"},owner:"msollami"},wren:{title:"Wren",owner:"clsource"},xeora:{title:"Xeora",require:"markup",alias:"xeoracube",aliasTitles:{xeoracube:"XeoraCube"},owner:"freakmaxi"},"xml-doc":{title:"XML doc (.net)",require:"markup",modify:["csharp","fsharp","vbnet"],owner:"RunDevelopment"},xojo:{title:"Xojo (REALbasic)",owner:"Golmote"},xquery:{title:"XQuery",require:"markup",owner:"Golmote"},yaml:{title:"YAML",alias:"yml",owner:"hason"},yang:{title:"YANG",owner:"RunDevelopment"},zig:{title:"Zig",owner:"RunDevelopment"}},plugins:{meta:{path:"plugins/{id}/prism-{id}",link:"plugins/{id}/"},"line-highlight":{title:"Line Highlight",description:"Highlights specific lines and/or line ranges."},"line-numbers":{title:"Line Numbers",description:"Line number at the beginning of code lines.",owner:"kuba-kubula"},"show-invisibles":{title:"Show Invisibles",description:"Show hidden characters such as tabs and line breaks.",optional:["autolinker","data-uri-highlight"]},autolinker:{title:"Autolinker",description:"Converts URLs and emails in code to clickable links. Parses Markdown links in comments."},wpd:{title:"WebPlatform Docs",description:'Makes tokens link to WebPlatform.org documentation. The links open in a new tab.'},"custom-class":{title:"Custom Class",description:"This plugin allows you to prefix Prism's default classes (.comment can become .namespace--comment) or replace them with your defined ones (like .editor__comment). You can even add new classes.",owner:"dvkndn",noCSS:!0},"file-highlight":{title:"File Highlight",description:"Fetch external files and highlight them with Prism. Used on the Prism website itself.",noCSS:!0},"show-language":{title:"Show Language",description:"Display the highlighted language in code blocks (inline code does not show the label).",owner:"nauzilus",noCSS:!0,require:"toolbar"},"jsonp-highlight":{title:"JSONP Highlight",description:"Fetch content with JSONP and highlight some interesting content (e.g. GitHub/Gists or Bitbucket API).",noCSS:!0,owner:"nauzilus"},"highlight-keywords":{title:"Highlight Keywords",description:"Adds special CSS classes for each keyword for fine-grained highlighting.",owner:"vkbansal",noCSS:!0},"remove-initial-line-feed":{title:"Remove initial line feed",description:"Removes the initial line feed in code blocks.",owner:"Golmote",noCSS:!0},"inline-color":{title:"Inline color",description:"Adds a small inline preview for colors in style sheets.",require:"css-extras",owner:"RunDevelopment"},previewers:{title:"Previewers",description:"Previewers for angles, colors, gradients, easing and time.",require:"css-extras",owner:"Golmote"},autoloader:{title:"Autoloader",description:"Automatically loads the needed languages to highlight the code blocks.",owner:"Golmote",noCSS:!0},"keep-markup":{title:"Keep Markup",description:"Prevents custom markup from being dropped out during highlighting.",owner:"Golmote",optional:"normalize-whitespace",noCSS:!0},"command-line":{title:"Command Line",description:"Display a command line with a prompt and, optionally, the output/response from the commands.",owner:"chriswells0"},"unescaped-markup":{title:"Unescaped Markup",description:"Write markup without having to escape anything."},"normalize-whitespace":{title:"Normalize Whitespace",description:"Supports multiple operations to normalize whitespace in code blocks.",owner:"zeitgeist87",optional:"unescaped-markup",noCSS:!0},"data-uri-highlight":{title:"Data-URI Highlight",description:"Highlights data-URI contents.",owner:"Golmote",noCSS:!0},toolbar:{title:"Toolbar",description:"Attach a toolbar for plugins to easily register buttons on the top of a code block.",owner:"mAAdhaTTah"},"copy-to-clipboard":{title:"Copy to Clipboard Button",description:"Add a button that copies the code block to the clipboard when clicked.",owner:"mAAdhaTTah",require:"toolbar",noCSS:!0},"download-button":{title:"Download Button",description:"A button in the toolbar of a code block adding a convenient way to download a code file.",owner:"Golmote",require:"toolbar",noCSS:!0},"match-braces":{title:"Match braces",description:"Highlights matching braces.",owner:"RunDevelopment"},"diff-highlight":{title:"Diff Highlight",description:"Highlights the code inside diff blocks.",owner:"RunDevelopment",require:"diff"},"filter-highlight-all":{title:"Filter highlightAll",description:"Filters the elements the highlightAll and highlightAllUnder methods actually highlight.",owner:"RunDevelopment",noCSS:!0},treeview:{title:"Treeview",description:"A language with special styles to highlight file system tree structures.",owner:"Golmote"}}})},2885:(e,t,n)=>{const r=n(9901),a=n(9642),o=new Set;function i(e){void 0===e?e=Object.keys(r.languages).filter((e=>"meta"!=e)):Array.isArray(e)||(e=[e]);const t=[...o,...Object.keys(Prism.languages)];a(r,e,t).load((e=>{if(!(e in r.languages))return void(i.silent||console.warn("Language does not exist: "+e));const t="./prism-"+e;delete n.c[n(6500).resolve(t)],delete Prism.languages[e],n(6500)(t),o.add(e)}))}i.silent=!1,e.exports=i},6726:(e,t,n)=>{var r={"./":2885};function a(e){var t=o(e);return n(t)}function o(e){if(!n.o(r,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return r[e]}a.keys=function(){return Object.keys(r)},a.resolve=o,e.exports=a,a.id=6726},6500:(e,t,n)=>{var r={"./":2885};function a(e){var t=o(e);return n(t)}function o(e){if(!n.o(r,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return r[e]}a.keys=function(){return Object.keys(r)},a.resolve=o,e.exports=a,a.id=6500},9642:e=>{"use strict";var t=function(){var e=function(){};function t(e,t){Array.isArray(e)?e.forEach(t):null!=e&&t(e,0)}function n(e){for(var t={},n=0,r=e.length;n "));var l={},s=e[r];if(s){function u(t){if(!(t in e))throw new Error(r+" depends on an unknown component "+t);if(!(t in l))for(var i in a(t,o),l[t]=!0,n[t])l[i]=!0}t(s.require,u),t(s.optional,u),t(s.modify,u)}n[r]=l,o.pop()}}return function(e){var t=n[e];return t||(a(e,r),t=n[e]),t}}function a(e){for(var t in e)return!0;return!1}return function(o,i,l){var s=function(e){var t={};for(var n in e){var r=e[n];for(var a in r)if("meta"!=a){var o=r[a];t[a]="string"==typeof o?{title:o}:o}}return t}(o),u=function(e){var n;return function(r){if(r in e)return r;if(!n)for(var a in n={},e){var o=e[a];t(o&&o.alias,(function(t){if(t in n)throw new Error(t+" cannot be alias for both "+a+" and "+n[t]);if(t in e)throw new Error(t+" cannot be alias of "+a+" because it is a component.");n[t]=a}))}return n[r]||r}}(s);i=i.map(u),l=(l||[]).map(u);var c=n(i),d=n(l);i.forEach((function e(n){var r=s[n];t(r&&r.require,(function(t){t in d||(c[t]=!0,e(t))}))}));for(var f,p=r(s),m=c;a(m);){for(var g in f={},m){var h=s[g];t(h&&h.modify,(function(e){e in d&&(f[e]=!0)}))}for(var b in d)if(!(b in c))for(var v in p(b))if(v in c){f[b]=!0;break}for(var y in m=f)c[y]=!0}var w={getIds:function(){var e=[];return w.load((function(t){e.push(t)})),e},load:function(t,n){return function(t,n,r,a){var o=a?a.series:void 0,i=a?a.parallel:e,l={},s={};function u(e){if(e in l)return l[e];s[e]=!0;var a,c=[];for(var d in t(e))d in n&&c.push(d);if(0===c.length)a=r(e);else{var f=i(c.map((function(e){var t=u(e);return delete s[e],t})));o?a=o(f,(function(){return r(e)})):r(e)}return l[e]=a}for(var c in n)u(c);var d=[];for(var f in s)d.push(l[f]);return i(d)}(p,c,t,n)}};return w}}();e.exports=t},2703:(e,t,n)=>{"use strict";var r=n(414);function a(){}function o(){}o.resetWarningCache=a,e.exports=function(){function e(e,t,n,a,o,i){if(i!==r){var l=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw l.name="Invariant Violation",l}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:o,resetWarningCache:a};return n.PropTypes=n,n}},5697:(e,t,n)=>{e.exports=n(2703)()},414:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},4448:(e,t,n)=>{"use strict";var r=n(7294),a=n(7418),o=n(3840);function i(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n