├── .gitignore ├── README.md ├── part1 ├── README.md ├── client │ ├── public │ │ ├── css │ │ │ └── application.css │ │ ├── favicon.ico │ │ ├── index.html │ │ └── javascript │ │ │ ├── application.js │ │ │ ├── components.js │ │ │ └── events.js │ └── src │ │ └── server.ts └── webservers │ ├── deno │ ├── dynamic.ts │ ├── hardcoded.ts │ └── static.ts │ ├── express │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── dynamic.js │ │ ├── hardcoded.js │ │ └── static.js │ └── public │ ├── index.html │ ├── linked.html │ └── styles.css ├── part2 └── README.md └── part3 └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UI Architecture 2 | 3 | ## Part One 4 | 5 | 1. Early Years 6 | 2. Requests 7 | 3. Managing State 8 | 4. Mutation 9 | 5. User Experience 10 | 6. Progressive Enhancement 11 | 7. Handling Events 12 | 8. Modifying The Document 13 | 9. Transport 14 | 10. The Platform 15 | 16 | ## Part Two 17 | 18 | 1. Complexity 19 | 2. Components 20 | 3. Managing State 21 | 4. Side Effects 22 | 5. Ecosystem 23 | 24 | ## Part Three 25 | 26 | 1. Abstraction 27 | 2. CSS 28 | 3. Tooling 29 | 4. Tailwind 30 | 5. CSS-in-JS -------------------------------------------------------------------------------- /part1/README.md: -------------------------------------------------------------------------------- 1 | # UI Architecture - Part One 2 | 3 | ## The Platform 4 | 5 | - Requests 6 | - Managing State 7 | - Mutation 8 | - User Experience 9 | - Progressive Enhancement 10 | - Handling Events 11 | - Modifying The Document 12 | - Transport 13 | 14 | ## 1. Early Years 15 | 16 | Sharing information is intrinsic to human communities. As technology advances, we use those advances to improve our ability to communicate information. 17 | 18 | In the early 90s, [Tim Berners-Lee](https://en.wikipedia.org/wiki/Tim_Berners-Lee) saw an opportunity to combine computer networking and information communication. He released a [proposal](https://www.w3.org/History/1989/proposal.html) for a HyperCard-inspired approach to linking documents. Hypertext (HTML) and the Hypertext Transfer Protocol (HTTP) provide the basis for the modern internet. In 1991 he posted to [alt.hypertext]([https://www.w3.](https://www.w3.org/People/Berners-Lee/1991/08/art-6484.txt)org/People/Berners-Lee/1991/08/art-6484.txt) an invite to visit the [first website](http://info.cern.ch/hypertext/WWW/TheProject.html) and use software he developed based on his proposal. 19 | 20 | In 1994, Netscape Communications Corporation was the first company to try and capitalize on the World Wide Web. Their Netscape Navigator browser grew in popularity quickly; it was superior to their competitors. A documentary, [Project Code Rush](https://www.youtube.com/watch?v=4Q7FTjhvZ7Y), describes the end of Netscape during their effort to release its browser code as open-source software. It is worth watching. 21 | 22 | The World Wide Web spread into people's homes, and at this point, anyone with an internet connection was likely using Netscape Navigator to browse documents described as HTML. A specification for HTML or HTTP would be incomplete when Netscape added them to their browser. 23 | 24 | Setting up a server using the software provided by Tim Berners-Lee was a barrier to entry for most people if they wanted to post content on the WWW. GeoCities, a web company that started as Beverly Hills Internet, became a very popular web hosting provider. 25 | 26 | Users choose a neighbourhood based on the topic of their website. When they sign up, they receive a four-digit address in that neighbourhood. Someone might find a website about computer games using the [URL](https://en.wikipedia.org/wiki/URL) http://www.geocities.com/SiliconValley/4336/. Yahoo purchased GeoCities for $3.57 billion worth of stock in January 1999. 27 | 28 | > Demo [neocities](https://neocities.karl.sh/) available on [GitHub](https://github.com/jensen/neocities/). 29 | 30 | ## 2. Requests 31 | 32 | When we start a web server, it opens a port and listens for TCP connections. A basic web server only performs work when a user agent makes a request. 33 | 34 | > Example of a hard-coded web servers with [express](./webservers/express/src/hardcoded.js) and [Deno](./webservers/deno/hardcoded.ts). 35 | 36 | This type of web server requires a restart whenever the contents of the document change. We can alter our web server code to read files and serve them as documents when we find them. 37 | 38 | > Example of a static web servers with [express](./webservers/express/src/static.js) and [Deno](./webservers/deno/static.ts). 39 | 40 | After visiting a URL, the server returns a document, and the browser parses the document and makes further requests for linked resources. If a user clicks on a link or submits a form, then the browser makes a request to a server. 41 | 42 | > Example showing linked requests in the Network tab. 43 | 44 | Apache and NGINX are the most common production web servers available today. These servers do an excellent job of serving static files. Another option is deploying static assets to a CDN so that users download the files from servers closer to them. With these approaches available, Rails does not serve static files in production by default. 45 | 46 | ## 3. Managing State 47 | 48 | The ability to request documents and view the latest version on demand proves valuable. We can turn to dynamic web servers when we only need to provide more dynamic information. A server generates a dynamic web page by constructing the content when a user makes a request. 49 | 50 | > Example of a dynamic web servers with [express](./webservers/express/src/dynamic.js) and [Deno](./webservers/deno/dynamic.ts). 51 | 52 | The server can return relevant data when we provide search parameters as part of the URL. It can also create links to other paths within the site using search parameters that allow us to pass state as part of the URL. 53 | 54 | Lou Montulli invented the cookie as a Netscape employee in 1994. Before browser cookies, it was tough to store user state between requests. A server creates a cookie in response to a request from the browser. The browser holds the cookie and sends it with every following request to that site. The server can replace the cookie when values need to be updated. 55 | 56 | ## 4. Mutation 57 | 58 | We introduce most of the complexity in web development by allowing users to mutate data. The most popular sites of our time have ways for users to contribute content. Unknown to most, they submit a form with side effects by using the POST method with their content forming the body data of the request. 59 | 60 | The [HTML 2.0](https://www.w3.org/MarkUp/html-spec/html-spec_toc.html) specification contains the `
` element. 61 | 62 | > Example of a [form with submit and reset](./webservers/src/dynamic.ts). Show redirect after post. 63 | 64 | Even though the [HTTP 1.1](https://www.rfc-editor.org/rfc/rfc7231) specification includes new methods for updating or destroying existing resources, the element does not support PUT or DELETE as a method attribute. Rails form helpers include a hidden named “_method” that tells the server how the router should handle the POST request. 65 | 66 | ## 5. User Experience 67 | 68 | Browser vendors' rapid adoption of [HTML 2.0](https://www.w3.org/MarkUp/html-spec/html-spec_toc.html) features quickly turned the WWW from a text-based platform to one that is capable of delivering a rich-media experience. 69 | 70 | One of the early challenges for developers was creating more complex layouts without any styling tools. In the period between HTML 2.0 and [HTML 3.2](https://www.w3.org/TR/2018/SPSD-html32-20180315/) with [CSS](https://www.w3.org/TR/CSS1/), a technique of slicing up images and displaying them as the background of table cells is considered normal. 71 | 72 | > Example of a [table-based layout](https://neocities.karl.sh/SiliconValley/1000/layout.html) on neocities. 73 | 74 | Adding Cascading Style Sheets means that designers can create the types of experiences we see today. A new challenge for developers is supporting multiple popular browser vendors that don’t all implement the CSS to the exact specifications. 75 | 76 | Adding more scripts, styles and media to a site takes its toll on an early 90s internet connection. A web server can implement a good [caching strategy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control), which instructs the browser on which content it can and for how long to cache it. 77 | 78 | > Example of caching in the browser, using neocities site. 79 | 80 | ## 6. Progressive Enhancement 81 | 82 | The term [Progressive Enhancement](https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement) is growing in popularity. It describes a design philosophy that has evolved to provide a baseline of functionality to as many users as possible. If a user visits with a modern browser, then the experience they have will be determined by the features the browser supports. We can use frameworks to help us build sites that support this idea. 83 | 84 | > Example with rails w/ turbo and remix. 85 | 86 | It is not only old browsers that may not be supported by today's modern sites. A portion of the population using low-powered mobile devices is excluded when basic support is not provided for basic features. These frameworks want to make it easier to make the decision to include this support. 87 | 88 | ## 7.Handling Events 89 | 90 | When a browser enables JavaScript, we can create an interactive experience without reloading the document. Attaching events to elements that listen for specific user actions allows us to build custom interactivity into our applications. 91 | 92 | > Example of the click event handler adding a new field to the form breakpoint using the Sources tab. 93 | 94 | Most bugs are the result of state mutation. We can use the developer tools to track down client-side bugs. 95 | 96 | ## 8. Modifying The Document 97 | 98 | We can manipulate what is currently rendered in the browser using the DOM API. 99 | 100 | > Demo Event Planner showing the updated DOM elements in the Elements tab. 101 | 102 | [React](https://reactjs.org/) uses a virtual DOM to quickly diff against a previous version of the DOM and then apply the changes as a reconciliation step. [Svelte](https://svelte.dev/) and [SolidJS](https://www.solidjs.com/) avoid using a virtual DOM by including a compile step that generates code to perform specific imperative updates when state changes. 103 | 104 | ## 9. Transport 105 | 106 | We can render the HTML for the elements we know about on the server, but we also need to update the DOM dynamically when there is a state mutation. 107 | 108 | > Demo Event Planner showing view source for the server-rendered initial list. 109 | 110 | We can list for a “submit” event that the `` element fires. We prevent the default submission behaviour and make an HTTP request using the [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API. 111 | 112 | > Example of the submit event being handled, making the request. 113 | 114 | The [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest) API has been relied on heavily over the past two decades but is unnecessary for new projects. 115 | 116 | ## 10. The Platform 117 | 118 | While there are infinite ways to build frameworks, the platform that we work with is finite. Frameworks play a significant role in modern web software development. They allow us to build sites faster by providing a convenient way to do common things with enough flexibility to customize features as needed. 119 | 120 | A good understanding of the [Web API](https://developer.mozilla.org/en-US/docs/Web/API) allows us to debug issues quicker. We can use the [tools](https://developer.chrome.com/docs/devtools/) built into our browser to reduce the time it takes to locate the source of a defect. The frameworks that we use 121 | 122 | Those responsible for the popular [V8](https://v8.dev/) runtimes, [Node](https://blog.appsignal.com/2022/04/26/nodejs-18-release-whats-new.html) and [Deno](https://deno.land/), are moving towards an API that is compatible with the browser environment. 123 | 124 | The World Wide Web comprises various specifications; frameworks and libraries inspire some of those specifications. We recognize that [components](https://developer.mozilla.org/en-US/docs/Web/Web_Components) are a core architecture pattern for user interfaces. We can use the platform's features directly, and we can create abstractions to speed up our development. -------------------------------------------------------------------------------- /part1/client/public/css/application.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: Arial, sans-serif; 3 | } 4 | 5 | fieldset { 6 | color: indigo; 7 | border-style: solid; 8 | border-color: darkslateblue; 9 | border-width: 4px; 10 | font-weight: 700; 11 | margin: 1rem 0; 12 | } 13 | 14 | li { 15 | margin: 1rem 0; 16 | } 17 | 18 | hr { 19 | border: 1px solid darkslateblue; 20 | } 21 | 22 | input { 23 | padding: 0.25rem 0.5rem; 24 | } 25 | 26 | ul { 27 | list-style: none; 28 | padding: 0; 29 | margin: 0; 30 | } 31 | 32 | button, 33 | input[type="submit"], 34 | input[type="reset"] { 35 | border: 0; 36 | padding: 0.25rem 0.5rem; 37 | background-color: indigo; 38 | color: white; 39 | border-radius: 0.25rem; 40 | } 41 | 42 | button:hover, 43 | input[type="submit"]:hover, 44 | input[type="reset"]:hover { 45 | background-color: darkslateblue; 46 | } 47 | 48 | [data-component="error"] { 49 | color: orangered; 50 | } 51 | 52 | [data-component*="remove-"] { 53 | margin-left: 1rem; 54 | } 55 | 56 | [data-component="party-list"] li { 57 | padding: 1rem; 58 | border: 4px solid indigo; 59 | } 60 | 61 | [data-component="party-list"] li div { 62 | margin-right: 2rem; 63 | } 64 | 65 | [data-component="party-list"] h3 { 66 | margin: 0; 67 | text-transform: uppercase; 68 | } 69 | 70 | [data-component="party-list"] h4 { 71 | margin: 0; 72 | color: gray; 73 | } 74 | 75 | .horizontal-flex { 76 | display: flex; 77 | } 78 | 79 | .vertical-flex { 80 | display: flex; 81 | flex-direction: column; 82 | } 83 | -------------------------------------------------------------------------------- /part1/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jensen/ui-workshop/81cde0397835d744aa8418c668d69fa6a05a6fe8/part1/client/public/favicon.ico -------------------------------------------------------------------------------- /part1/client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | JavaScript Forms 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

EventPlanner

19 |
20 | 21 |
22 | Attendees 23 | 27 | 28 | 29 |
    30 |
31 |
32 |
33 | Details 34 | 38 |
39 | 43 |
44 |

45 |
46 | 47 | 48 | 49 | 68 |
69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /part1/client/public/javascript/application.js: -------------------------------------------------------------------------------- 1 | import { FormComponent, AddButtonComponent } from "./components.js"; 2 | import { 3 | handleAddButtonClick, 4 | handleFormReset, 5 | handleFormSubmission, 6 | } from "./events.js"; 7 | 8 | FormComponent().addEventListener("reset", handleFormReset); 9 | FormComponent().addEventListener("submit", handleFormSubmission); 10 | FormComponent().addEventListener("keydown", (event) => { 11 | if (event.key === "Enter") { 12 | event.preventDefault(); 13 | handleAddButtonClick(event); 14 | } 15 | }); 16 | AddButtonComponent().addEventListener("click", handleAddButtonClick); 17 | -------------------------------------------------------------------------------- /part1/client/public/javascript/components.js: -------------------------------------------------------------------------------- 1 | /* 2 | These are cached values for the elements. It is also possible 3 | to call document.querySelector each time you want to find one 4 | of these elements. This could be slow with a large document. 5 | 6 | If we use "id" instead of "data-component" the browser will 7 | create references to these elements in the window object. 8 | */ 9 | const elements = { 10 | list: document.querySelector("[data-component='list']"), 11 | form: document.querySelector("[data-component='form']"), 12 | addButton: document.querySelector("[data-component='add-button']"), 13 | nameInput: document.querySelector("[data-component='name-input']"), 14 | error: document.querySelector("[data-component='error']"), 15 | partyList: document.querySelector("[data-component='party-list']"), 16 | parties: {}, 17 | items: {}, 18 | remove: {}, 19 | }; 20 | 21 | /* 22 | Each component references a specific existing element in the DOM. 23 | */ 24 | export const ListComponent = () => elements.list; 25 | export const FormComponent = () => elements.form; 26 | export const AddButtonComponent = () => elements.addButton; 27 | export const NameInputComponent = () => elements.nameInput; 28 | export const ErrorComponent = () => elements.error; 29 | 30 | /* 31 | These components create elements dynamically. They have a unique identifier 32 | when they are in a list, and can be passed data to be when creating elements. 33 | */ 34 | export const ItemComponent = (key, props) => { 35 | /* Check to see if we have this in our index of elements. */ 36 | if (elements.items[key]) { 37 | return elements.items[key]; 38 | } 39 | 40 | /* Create the element to display the name of the attendee. */ 41 | const item = document.createElement("li"); 42 | 43 | item.setAttribute("data-component", `item-${key}`); 44 | 45 | /* 46 | The values being added with the input field can be added to the form 47 | as hidden fields. 48 | */ 49 | const input = document.createElement("input"); 50 | 51 | input.setAttribute("type", "hidden"); 52 | input.setAttribute("name", "attendees"); 53 | input.setAttribute("value", props.name); 54 | 55 | /* Add the hidden input, display text and remove button to the item. */ 56 | item.appendChild(input); 57 | item.appendChild(document.createTextNode(props.name)); 58 | item.appendChild(RemoveComponent(key)); 59 | 60 | /* Index the element. */ 61 | elements.items[key] = item; 62 | 63 | return item; 64 | }; 65 | 66 | export const RemoveComponent = (key, props) => { 67 | if (elements.remove[key]) { 68 | return elements.remove[key]; 69 | } 70 | 71 | const button = document.createElement("button"); 72 | 73 | button.setAttribute("data-component", `remove-${key}`); 74 | button.appendChild(document.createTextNode("remove")); 75 | 76 | button.addEventListener("click", (event) => { 77 | ItemComponent(key).remove(); 78 | 79 | delete elements.items[key]; 80 | delete elements.remove[key]; 81 | }); 82 | 83 | return button; 84 | }; 85 | 86 | export const PartyListComponent = () => elements.partyList; 87 | 88 | export const PartyComponent = (key, props) => { 89 | if (elements.parties[key]) { 90 | return elements.parties[key]; 91 | } 92 | 93 | const template = document.querySelector( 94 | "[data-component='party-item-template']" 95 | ); 96 | const item = template.content.cloneNode(true); 97 | 98 | const date = item.querySelector("[data-component-slot='party-item-date']"); 99 | const pizzas = item.querySelector( 100 | "[data-component-slot='party-item-pizzas']" 101 | ); 102 | const guestlist = item.querySelector( 103 | "[data-component-slot='party-item-guestlist']" 104 | ); 105 | 106 | date.textContent = props.date; 107 | pizzas.textContent = props.pizzas; 108 | guestlist.textContent = props.guestlist.join(", "); 109 | 110 | elements.parties[key] = item; 111 | 112 | PartyListComponent().appendChild(item); 113 | 114 | return item; 115 | }; 116 | -------------------------------------------------------------------------------- /part1/client/public/javascript/events.js: -------------------------------------------------------------------------------- 1 | import { 2 | ListComponent, 3 | ItemComponent, 4 | NameInputComponent, 5 | ErrorComponent, 6 | PartyListComponent, 7 | PartyComponent, 8 | FormComponent, 9 | } from "./components.js"; 10 | 11 | export const handleFormSubmission = async (event) => { 12 | /* 13 | The default behaviour of a form when we trigger a submit 14 | event is to retrieve a new document. Since this is a 15 | single page application, we need to stay withing this 16 | document. We prevent the default behaviour when javascript 17 | is enabled. 18 | */ 19 | event.preventDefault(); 20 | 21 | /* 22 | FormData 23 | Reference: https://developer.mozilla.org/en-US/docs/Web/API/FormData 24 | */ 25 | const body = new FormData(event.target); 26 | 27 | /* 28 | Basic *client* side validation. The problem with client side validation 29 | is that it doesn't matter. The trade off is that we need to submit the 30 | form data to get the errors back from the server. 31 | 32 | In some cases it is a good idea to validate on the client before sending 33 | data to the server. This should only be done to improve user experience. 34 | 35 | const date = body.get("date"); 36 | const pizzas = body.get("pizzas"); 37 | const attendees = body.getAll("attendees"); 38 | 39 | const errors = []; 40 | 41 | if (attendees.length < 2) { 42 | errors.push("Must invite a minimum of 2 people"); 43 | } 44 | 45 | if (!date) { 46 | errors.push("Date must be set"); 47 | } 48 | 49 | if (Number(pizzas) / attendees.length < 0.5) { 50 | errors.push("Must have at least half of a pizza per person"); 51 | } 52 | 53 | if (errors.length > 0) { 54 | ErrorComponent().innerText = errors.join(", "); 55 | return; 56 | } 57 | 58 | ErrorComponent().innerText = ""; 59 | */ 60 | 61 | /* 62 | Fetch 63 | Reference: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API 64 | */ 65 | const response = await fetch("/api/create", { 66 | method: "post", 67 | body, 68 | }); 69 | 70 | /* 71 | Check that thes status code for the response is expecteed. 72 | */ 73 | 74 | if (response.status === 201) { 75 | const party = await response.json(); 76 | 77 | PartyListComponent().appendChild(PartyComponent(party.id, party)); 78 | ErrorComponent().innerText = ""; 79 | FormComponent().reset(); 80 | 81 | return; 82 | } 83 | 84 | /* 85 | Render the list of errors provided by the server. 86 | */ 87 | 88 | const errors = await response.json(); 89 | 90 | ErrorComponent().innerText = errors.join(", "); 91 | }; 92 | 93 | export const handleFormReset = (event) => { 94 | ListComponent().replaceChildren(); 95 | }; 96 | 97 | export const handleAddButtonClick = (event) => { 98 | const name = NameInputComponent().value; 99 | 100 | /* 101 | Since adding items to the list is only being handled with 102 | JavaScript, we can do a check ot make sure that the value 103 | is not empty here, but we also need to check on the server 104 | once the request is submitted. 105 | */ 106 | if (!name) { 107 | ErrorComponent().innerText = "Name must not be empty"; 108 | return; 109 | } 110 | 111 | ErrorComponent().innerText = ""; 112 | 113 | const Item = ItemComponent(crypto.randomUUID(), { 114 | name, 115 | }); 116 | 117 | ListComponent().appendChild(Item); 118 | 119 | NameInputComponent().value = ""; 120 | }; 121 | -------------------------------------------------------------------------------- /part1/client/src/server.ts: -------------------------------------------------------------------------------- 1 | import * as server from "https://deno.land/std@0.127.0/http/server.ts"; 2 | import * as path from "https://deno.land/std@0.127.0/path/mod.ts"; 3 | 4 | const mimetypes: { [key: string]: string } = { 5 | ".html": "text/html", 6 | ".js": "application/javascript", 7 | ".css": "text/css", 8 | ".ico": "image/x-icon", 9 | }; 10 | 11 | interface IParty { 12 | id: string; 13 | guestlist: string[]; 14 | date: string; 15 | pizzas: number; 16 | } 17 | 18 | const parties: IParty[] = []; 19 | 20 | const tagPartyList = ""; 21 | const renderPartyList = (list: IParty[]) => 22 | list 23 | .map( 24 | (party) => 25 | ` 26 |
  • 27 |
    28 |

    Date

    29 |

    ${party.date}

    30 |
    31 |
    32 |

    Pizzas

    33 |

    ${party.pizzas}

    34 |
    35 |
    36 |

    Gueslist

    37 |

    ${party.guestlist.join(", ")}

    38 |
    39 |
  • 40 | ` 41 | ) 42 | .join("\n"); 43 | 44 | await server.serve( 45 | /* 46 | Request 47 | Reference: https://developer.mozilla.org/en-US/docs/Web/API/Request 48 | */ 49 | async (request: Request) => { 50 | /* 51 | URL 52 | Reference: https://developer.mozilla.org/en-US/docs/Web/API/URL 53 | */ 54 | const url = new URL(request.url); 55 | 56 | if (request.method === "POST" && url.pathname === "/api/create") { 57 | /* 58 | FormData 59 | Reference: https://developer.mozilla.org/en-US/docs/Web/API/FormData 60 | */ 61 | const body = await request.formData(); 62 | 63 | const party: IParty = { 64 | /* 65 | Crypto 66 | Reference: https://developer.mozilla.org/en-US/docs/Web/API/Crypto 67 | */ 68 | id: crypto.randomUUID(), 69 | guestlist: body.getAll("attendees") as string[], 70 | date: body.get("date") as string, 71 | pizzas: Number(body.get("pizzas")), 72 | }; 73 | 74 | const errors: string[] = []; 75 | 76 | if (party.guestlist.some((name) => name === "")) { 77 | errors.push("All attendees must have a name"); 78 | } 79 | 80 | if (party.guestlist.length < 2) { 81 | errors.push("Must invite a minimum of 2 people"); 82 | } 83 | 84 | if (!party.date) { 85 | errors.push("Date must be set"); 86 | } 87 | 88 | if (party.pizzas / party.guestlist.length < 0.5) { 89 | errors.push("Must have at least half of a pizza per person"); 90 | } 91 | 92 | if (errors.length > 0) { 93 | return new Response(JSON.stringify(errors), { 94 | status: 400, 95 | headers: { 96 | "Content-Type": "application/json", 97 | }, 98 | }); 99 | } 100 | 101 | parties.push(party); 102 | 103 | /* 104 | Response 105 | Reference: https://developer.mozilla.org/en-US/docs/Web/API/Response 106 | */ 107 | return new Response(JSON.stringify(party), { 108 | status: 201, 109 | headers: { 110 | "Content-Type": "application/json", 111 | }, 112 | }); 113 | } 114 | 115 | if (request.method === "GET" && url.pathname === "/api/list") { 116 | /* 117 | Response 118 | Reference: https://developer.mozilla.org/en-US/docs/Web/API/Response 119 | */ 120 | return new Response(JSON.stringify(parties), { 121 | status: 200, 122 | headers: { 123 | "Content-Type": "application/json", 124 | }, 125 | }); 126 | } 127 | 128 | const filename = 129 | url.pathname === "/" 130 | ? path.resolve("./public/index.html") 131 | : path.resolve("./public" + url.pathname); 132 | 133 | try { 134 | const file = await Deno.readTextFile(filename); 135 | const extension = filename.substring(filename.lastIndexOf(".")); 136 | 137 | /* 138 | Response 139 | Reference: https://developer.mozilla.org/en-US/docs/Web/API/Response 140 | */ 141 | return new Response( 142 | filename.endsWith("index.html") 143 | ? file.replace(tagPartyList, renderPartyList(parties)) 144 | : file, 145 | { 146 | status: 200, 147 | headers: { 148 | "Content-Type": mimetypes[extension], 149 | "Cache-Control": "public, max-age=3600", 150 | }, 151 | } 152 | ); 153 | } catch (error) { 154 | /* 155 | Response 156 | Reference: https://developer.mozilla.org/en-US/docs/Web/API/Response 157 | */ 158 | return new Response("Not Found", { status: 404 }); 159 | } 160 | }, 161 | { 162 | port: 3000, 163 | } 164 | ); 165 | -------------------------------------------------------------------------------- /part1/webservers/deno/dynamic.ts: -------------------------------------------------------------------------------- 1 | import * as server from "https://deno.land/std@0.127.0/http/server.ts"; 2 | 3 | const serverConfig = { 4 | port: 3000, 5 | }; 6 | 7 | interface IDataItem { 8 | id: string; 9 | name: string; 10 | } 11 | 12 | const data: IDataItem[] = [ 13 | { id: crypto.randomUUID(), name: "First" }, 14 | { id: crypto.randomUUID(), name: "Second" }, 15 | { id: crypto.randomUUID(), name: "Third" }, 16 | { id: crypto.randomUUID(), name: "Fourth" }, 17 | { id: crypto.randomUUID(), name: "Fifth" }, 18 | ]; 19 | 20 | const template = (items: IDataItem[], error?: string) => ` 21 |
    22 | 23 | 24 | 25 |
    26 | ${error ? `

    ${error}

    ` : ""} 27 | 30 | `; 31 | 32 | const indexHandler = (request: Request) => { 33 | const url = new URL(request.url); 34 | const error = url.searchParams.get("error") || undefined; 35 | 36 | return new Response(template(data, error), { 37 | status: 200, 38 | headers: { 39 | "Content-Type": "text/html", 40 | }, 41 | }); 42 | }; 43 | 44 | const createHandler = async (request: Request) => { 45 | const body = await request.formData(); 46 | const item = body.get("item") || ""; 47 | 48 | if (item) { 49 | data.push({ 50 | id: crypto.randomUUID(), 51 | name: item as string, 52 | }); 53 | } 54 | 55 | const error = item === "" ? "?error=Item cannot be blank" : ""; 56 | 57 | return new Response("", { 58 | status: 303, 59 | headers: { 60 | Location: `/items${error}`, 61 | }, 62 | }); 63 | }; 64 | 65 | type RequestHandler = (request: Request) => Response | Promise; 66 | 67 | interface MethodHandler { 68 | [key: string]: RequestHandler; 69 | } 70 | 71 | const handlers: { [key: string]: MethodHandler } = { 72 | GET: { 73 | "/": indexHandler, 74 | "/items": indexHandler, 75 | }, 76 | POST: { 77 | "/items": createHandler, 78 | }, 79 | }; 80 | 81 | await server.serve(async (request: Request) => { 82 | const methodHandler = handlers[request.method]; 83 | 84 | if (!methodHandler) { 85 | return new Response("Method Not Allowed", { status: 405 }); 86 | } 87 | 88 | const pathname = new URL(request.url).pathname; 89 | const handler = await methodHandler[pathname]; 90 | 91 | if (handler) { 92 | return handler(request); 93 | } 94 | 95 | return new Response("Not Found", { 96 | status: 404, 97 | }); 98 | }, serverConfig); 99 | -------------------------------------------------------------------------------- /part1/webservers/deno/hardcoded.ts: -------------------------------------------------------------------------------- 1 | import * as server from "https://deno.land/std@0.122.0/http/server.ts"; 2 | 3 | const hardcoded = ` 4 | 5 | 6 | 7 | 8 | Basic Document 9 | 10 | 11 | 12 |

    Basic Document

    13 | 14 | 15 | 16 | `; 17 | 18 | await server.serve( 19 | async (request: Request) => { 20 | return new Response(hardcoded, { 21 | status: 200, 22 | headers: { 23 | "Content-Type": "text/html", 24 | }, 25 | }); 26 | }, 27 | { 28 | port: 3000, 29 | } 30 | ); 31 | -------------------------------------------------------------------------------- /part1/webservers/deno/static.ts: -------------------------------------------------------------------------------- 1 | import * as server from "https://deno.land/std@0.127.0/http/server.ts"; 2 | import * as path from "https://deno.land/std@0.127.0/path/mod.ts"; 3 | 4 | await server.serve( 5 | async (request: Request) => { 6 | const url = new URL(request.url); 7 | 8 | const filename = 9 | url.pathname === "/" 10 | ? path.resolve("../public/index.html") 11 | : path.resolve("../public" + url.pathname); 12 | 13 | try { 14 | const html = await Deno.readTextFile(filename); 15 | 16 | return new Response(html, { 17 | status: 200, 18 | headers: { 19 | "Content-Type": filename.endsWith(".css") ? "text/css" : "text/html", 20 | }, 21 | }); 22 | } catch (error) { 23 | return new Response("Not Found", { 24 | status: 404, 25 | headers: { 26 | "Content-Type": "text/plain", 27 | }, 28 | }); 29 | } 30 | }, 31 | { 32 | port: 3000, 33 | } 34 | ); 35 | -------------------------------------------------------------------------------- /part1/webservers/express/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-servers", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "web-servers", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "body-parser": "^1.20.0", 13 | "express": "^4.18.1" 14 | } 15 | }, 16 | "node_modules/accepts": { 17 | "version": "1.3.8", 18 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 19 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 20 | "dependencies": { 21 | "mime-types": "~2.1.34", 22 | "negotiator": "0.6.3" 23 | }, 24 | "engines": { 25 | "node": ">= 0.6" 26 | } 27 | }, 28 | "node_modules/array-flatten": { 29 | "version": "1.1.1", 30 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 31 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 32 | }, 33 | "node_modules/body-parser": { 34 | "version": "1.20.0", 35 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", 36 | "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", 37 | "dependencies": { 38 | "bytes": "3.1.2", 39 | "content-type": "~1.0.4", 40 | "debug": "2.6.9", 41 | "depd": "2.0.0", 42 | "destroy": "1.2.0", 43 | "http-errors": "2.0.0", 44 | "iconv-lite": "0.4.24", 45 | "on-finished": "2.4.1", 46 | "qs": "6.10.3", 47 | "raw-body": "2.5.1", 48 | "type-is": "~1.6.18", 49 | "unpipe": "1.0.0" 50 | }, 51 | "engines": { 52 | "node": ">= 0.8", 53 | "npm": "1.2.8000 || >= 1.4.16" 54 | } 55 | }, 56 | "node_modules/bytes": { 57 | "version": "3.1.2", 58 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 59 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 60 | "engines": { 61 | "node": ">= 0.8" 62 | } 63 | }, 64 | "node_modules/call-bind": { 65 | "version": "1.0.2", 66 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 67 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 68 | "dependencies": { 69 | "function-bind": "^1.1.1", 70 | "get-intrinsic": "^1.0.2" 71 | }, 72 | "funding": { 73 | "url": "https://github.com/sponsors/ljharb" 74 | } 75 | }, 76 | "node_modules/content-disposition": { 77 | "version": "0.5.4", 78 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 79 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 80 | "dependencies": { 81 | "safe-buffer": "5.2.1" 82 | }, 83 | "engines": { 84 | "node": ">= 0.6" 85 | } 86 | }, 87 | "node_modules/content-type": { 88 | "version": "1.0.4", 89 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 90 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", 91 | "engines": { 92 | "node": ">= 0.6" 93 | } 94 | }, 95 | "node_modules/cookie": { 96 | "version": "0.5.0", 97 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 98 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", 99 | "engines": { 100 | "node": ">= 0.6" 101 | } 102 | }, 103 | "node_modules/cookie-signature": { 104 | "version": "1.0.6", 105 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 106 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 107 | }, 108 | "node_modules/debug": { 109 | "version": "2.6.9", 110 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 111 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 112 | "dependencies": { 113 | "ms": "2.0.0" 114 | } 115 | }, 116 | "node_modules/depd": { 117 | "version": "2.0.0", 118 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 119 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 120 | "engines": { 121 | "node": ">= 0.8" 122 | } 123 | }, 124 | "node_modules/destroy": { 125 | "version": "1.2.0", 126 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 127 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 128 | "engines": { 129 | "node": ">= 0.8", 130 | "npm": "1.2.8000 || >= 1.4.16" 131 | } 132 | }, 133 | "node_modules/ee-first": { 134 | "version": "1.1.1", 135 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 136 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 137 | }, 138 | "node_modules/encodeurl": { 139 | "version": "1.0.2", 140 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 141 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", 142 | "engines": { 143 | "node": ">= 0.8" 144 | } 145 | }, 146 | "node_modules/escape-html": { 147 | "version": "1.0.3", 148 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 149 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 150 | }, 151 | "node_modules/etag": { 152 | "version": "1.8.1", 153 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 154 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 155 | "engines": { 156 | "node": ">= 0.6" 157 | } 158 | }, 159 | "node_modules/express": { 160 | "version": "4.18.1", 161 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", 162 | "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", 163 | "dependencies": { 164 | "accepts": "~1.3.8", 165 | "array-flatten": "1.1.1", 166 | "body-parser": "1.20.0", 167 | "content-disposition": "0.5.4", 168 | "content-type": "~1.0.4", 169 | "cookie": "0.5.0", 170 | "cookie-signature": "1.0.6", 171 | "debug": "2.6.9", 172 | "depd": "2.0.0", 173 | "encodeurl": "~1.0.2", 174 | "escape-html": "~1.0.3", 175 | "etag": "~1.8.1", 176 | "finalhandler": "1.2.0", 177 | "fresh": "0.5.2", 178 | "http-errors": "2.0.0", 179 | "merge-descriptors": "1.0.1", 180 | "methods": "~1.1.2", 181 | "on-finished": "2.4.1", 182 | "parseurl": "~1.3.3", 183 | "path-to-regexp": "0.1.7", 184 | "proxy-addr": "~2.0.7", 185 | "qs": "6.10.3", 186 | "range-parser": "~1.2.1", 187 | "safe-buffer": "5.2.1", 188 | "send": "0.18.0", 189 | "serve-static": "1.15.0", 190 | "setprototypeof": "1.2.0", 191 | "statuses": "2.0.1", 192 | "type-is": "~1.6.18", 193 | "utils-merge": "1.0.1", 194 | "vary": "~1.1.2" 195 | }, 196 | "engines": { 197 | "node": ">= 0.10.0" 198 | } 199 | }, 200 | "node_modules/finalhandler": { 201 | "version": "1.2.0", 202 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 203 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 204 | "dependencies": { 205 | "debug": "2.6.9", 206 | "encodeurl": "~1.0.2", 207 | "escape-html": "~1.0.3", 208 | "on-finished": "2.4.1", 209 | "parseurl": "~1.3.3", 210 | "statuses": "2.0.1", 211 | "unpipe": "~1.0.0" 212 | }, 213 | "engines": { 214 | "node": ">= 0.8" 215 | } 216 | }, 217 | "node_modules/forwarded": { 218 | "version": "0.2.0", 219 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 220 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 221 | "engines": { 222 | "node": ">= 0.6" 223 | } 224 | }, 225 | "node_modules/fresh": { 226 | "version": "0.5.2", 227 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 228 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 229 | "engines": { 230 | "node": ">= 0.6" 231 | } 232 | }, 233 | "node_modules/function-bind": { 234 | "version": "1.1.1", 235 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 236 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 237 | }, 238 | "node_modules/get-intrinsic": { 239 | "version": "1.1.3", 240 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", 241 | "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", 242 | "dependencies": { 243 | "function-bind": "^1.1.1", 244 | "has": "^1.0.3", 245 | "has-symbols": "^1.0.3" 246 | }, 247 | "funding": { 248 | "url": "https://github.com/sponsors/ljharb" 249 | } 250 | }, 251 | "node_modules/has": { 252 | "version": "1.0.3", 253 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 254 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 255 | "dependencies": { 256 | "function-bind": "^1.1.1" 257 | }, 258 | "engines": { 259 | "node": ">= 0.4.0" 260 | } 261 | }, 262 | "node_modules/has-symbols": { 263 | "version": "1.0.3", 264 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 265 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 266 | "engines": { 267 | "node": ">= 0.4" 268 | }, 269 | "funding": { 270 | "url": "https://github.com/sponsors/ljharb" 271 | } 272 | }, 273 | "node_modules/http-errors": { 274 | "version": "2.0.0", 275 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 276 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 277 | "dependencies": { 278 | "depd": "2.0.0", 279 | "inherits": "2.0.4", 280 | "setprototypeof": "1.2.0", 281 | "statuses": "2.0.1", 282 | "toidentifier": "1.0.1" 283 | }, 284 | "engines": { 285 | "node": ">= 0.8" 286 | } 287 | }, 288 | "node_modules/iconv-lite": { 289 | "version": "0.4.24", 290 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 291 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 292 | "dependencies": { 293 | "safer-buffer": ">= 2.1.2 < 3" 294 | }, 295 | "engines": { 296 | "node": ">=0.10.0" 297 | } 298 | }, 299 | "node_modules/inherits": { 300 | "version": "2.0.4", 301 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 302 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 303 | }, 304 | "node_modules/ipaddr.js": { 305 | "version": "1.9.1", 306 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 307 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 308 | "engines": { 309 | "node": ">= 0.10" 310 | } 311 | }, 312 | "node_modules/media-typer": { 313 | "version": "0.3.0", 314 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 315 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", 316 | "engines": { 317 | "node": ">= 0.6" 318 | } 319 | }, 320 | "node_modules/merge-descriptors": { 321 | "version": "1.0.1", 322 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 323 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 324 | }, 325 | "node_modules/methods": { 326 | "version": "1.1.2", 327 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 328 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 329 | "engines": { 330 | "node": ">= 0.6" 331 | } 332 | }, 333 | "node_modules/mime": { 334 | "version": "1.6.0", 335 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 336 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", 337 | "bin": { 338 | "mime": "cli.js" 339 | }, 340 | "engines": { 341 | "node": ">=4" 342 | } 343 | }, 344 | "node_modules/mime-db": { 345 | "version": "1.52.0", 346 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 347 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 348 | "engines": { 349 | "node": ">= 0.6" 350 | } 351 | }, 352 | "node_modules/mime-types": { 353 | "version": "2.1.35", 354 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 355 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 356 | "dependencies": { 357 | "mime-db": "1.52.0" 358 | }, 359 | "engines": { 360 | "node": ">= 0.6" 361 | } 362 | }, 363 | "node_modules/ms": { 364 | "version": "2.0.0", 365 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 366 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 367 | }, 368 | "node_modules/negotiator": { 369 | "version": "0.6.3", 370 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 371 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", 372 | "engines": { 373 | "node": ">= 0.6" 374 | } 375 | }, 376 | "node_modules/object-inspect": { 377 | "version": "1.12.2", 378 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", 379 | "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", 380 | "funding": { 381 | "url": "https://github.com/sponsors/ljharb" 382 | } 383 | }, 384 | "node_modules/on-finished": { 385 | "version": "2.4.1", 386 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 387 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 388 | "dependencies": { 389 | "ee-first": "1.1.1" 390 | }, 391 | "engines": { 392 | "node": ">= 0.8" 393 | } 394 | }, 395 | "node_modules/parseurl": { 396 | "version": "1.3.3", 397 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 398 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 399 | "engines": { 400 | "node": ">= 0.8" 401 | } 402 | }, 403 | "node_modules/path-to-regexp": { 404 | "version": "0.1.7", 405 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 406 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 407 | }, 408 | "node_modules/proxy-addr": { 409 | "version": "2.0.7", 410 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 411 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 412 | "dependencies": { 413 | "forwarded": "0.2.0", 414 | "ipaddr.js": "1.9.1" 415 | }, 416 | "engines": { 417 | "node": ">= 0.10" 418 | } 419 | }, 420 | "node_modules/qs": { 421 | "version": "6.10.3", 422 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", 423 | "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", 424 | "dependencies": { 425 | "side-channel": "^1.0.4" 426 | }, 427 | "engines": { 428 | "node": ">=0.6" 429 | }, 430 | "funding": { 431 | "url": "https://github.com/sponsors/ljharb" 432 | } 433 | }, 434 | "node_modules/range-parser": { 435 | "version": "1.2.1", 436 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 437 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 438 | "engines": { 439 | "node": ">= 0.6" 440 | } 441 | }, 442 | "node_modules/raw-body": { 443 | "version": "2.5.1", 444 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 445 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 446 | "dependencies": { 447 | "bytes": "3.1.2", 448 | "http-errors": "2.0.0", 449 | "iconv-lite": "0.4.24", 450 | "unpipe": "1.0.0" 451 | }, 452 | "engines": { 453 | "node": ">= 0.8" 454 | } 455 | }, 456 | "node_modules/safe-buffer": { 457 | "version": "5.2.1", 458 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 459 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 460 | "funding": [ 461 | { 462 | "type": "github", 463 | "url": "https://github.com/sponsors/feross" 464 | }, 465 | { 466 | "type": "patreon", 467 | "url": "https://www.patreon.com/feross" 468 | }, 469 | { 470 | "type": "consulting", 471 | "url": "https://feross.org/support" 472 | } 473 | ] 474 | }, 475 | "node_modules/safer-buffer": { 476 | "version": "2.1.2", 477 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 478 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 479 | }, 480 | "node_modules/send": { 481 | "version": "0.18.0", 482 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 483 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 484 | "dependencies": { 485 | "debug": "2.6.9", 486 | "depd": "2.0.0", 487 | "destroy": "1.2.0", 488 | "encodeurl": "~1.0.2", 489 | "escape-html": "~1.0.3", 490 | "etag": "~1.8.1", 491 | "fresh": "0.5.2", 492 | "http-errors": "2.0.0", 493 | "mime": "1.6.0", 494 | "ms": "2.1.3", 495 | "on-finished": "2.4.1", 496 | "range-parser": "~1.2.1", 497 | "statuses": "2.0.1" 498 | }, 499 | "engines": { 500 | "node": ">= 0.8.0" 501 | } 502 | }, 503 | "node_modules/send/node_modules/ms": { 504 | "version": "2.1.3", 505 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 506 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 507 | }, 508 | "node_modules/serve-static": { 509 | "version": "1.15.0", 510 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 511 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 512 | "dependencies": { 513 | "encodeurl": "~1.0.2", 514 | "escape-html": "~1.0.3", 515 | "parseurl": "~1.3.3", 516 | "send": "0.18.0" 517 | }, 518 | "engines": { 519 | "node": ">= 0.8.0" 520 | } 521 | }, 522 | "node_modules/setprototypeof": { 523 | "version": "1.2.0", 524 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 525 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 526 | }, 527 | "node_modules/side-channel": { 528 | "version": "1.0.4", 529 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 530 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 531 | "dependencies": { 532 | "call-bind": "^1.0.0", 533 | "get-intrinsic": "^1.0.2", 534 | "object-inspect": "^1.9.0" 535 | }, 536 | "funding": { 537 | "url": "https://github.com/sponsors/ljharb" 538 | } 539 | }, 540 | "node_modules/statuses": { 541 | "version": "2.0.1", 542 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 543 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 544 | "engines": { 545 | "node": ">= 0.8" 546 | } 547 | }, 548 | "node_modules/toidentifier": { 549 | "version": "1.0.1", 550 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 551 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 552 | "engines": { 553 | "node": ">=0.6" 554 | } 555 | }, 556 | "node_modules/type-is": { 557 | "version": "1.6.18", 558 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 559 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 560 | "dependencies": { 561 | "media-typer": "0.3.0", 562 | "mime-types": "~2.1.24" 563 | }, 564 | "engines": { 565 | "node": ">= 0.6" 566 | } 567 | }, 568 | "node_modules/unpipe": { 569 | "version": "1.0.0", 570 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 571 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 572 | "engines": { 573 | "node": ">= 0.8" 574 | } 575 | }, 576 | "node_modules/utils-merge": { 577 | "version": "1.0.1", 578 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 579 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 580 | "engines": { 581 | "node": ">= 0.4.0" 582 | } 583 | }, 584 | "node_modules/vary": { 585 | "version": "1.1.2", 586 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 587 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 588 | "engines": { 589 | "node": ">= 0.8" 590 | } 591 | } 592 | }, 593 | "dependencies": { 594 | "accepts": { 595 | "version": "1.3.8", 596 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", 597 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", 598 | "requires": { 599 | "mime-types": "~2.1.34", 600 | "negotiator": "0.6.3" 601 | } 602 | }, 603 | "array-flatten": { 604 | "version": "1.1.1", 605 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 606 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" 607 | }, 608 | "body-parser": { 609 | "version": "1.20.0", 610 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", 611 | "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", 612 | "requires": { 613 | "bytes": "3.1.2", 614 | "content-type": "~1.0.4", 615 | "debug": "2.6.9", 616 | "depd": "2.0.0", 617 | "destroy": "1.2.0", 618 | "http-errors": "2.0.0", 619 | "iconv-lite": "0.4.24", 620 | "on-finished": "2.4.1", 621 | "qs": "6.10.3", 622 | "raw-body": "2.5.1", 623 | "type-is": "~1.6.18", 624 | "unpipe": "1.0.0" 625 | } 626 | }, 627 | "bytes": { 628 | "version": "3.1.2", 629 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 630 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" 631 | }, 632 | "call-bind": { 633 | "version": "1.0.2", 634 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 635 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 636 | "requires": { 637 | "function-bind": "^1.1.1", 638 | "get-intrinsic": "^1.0.2" 639 | } 640 | }, 641 | "content-disposition": { 642 | "version": "0.5.4", 643 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", 644 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", 645 | "requires": { 646 | "safe-buffer": "5.2.1" 647 | } 648 | }, 649 | "content-type": { 650 | "version": "1.0.4", 651 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 652 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 653 | }, 654 | "cookie": { 655 | "version": "0.5.0", 656 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", 657 | "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" 658 | }, 659 | "cookie-signature": { 660 | "version": "1.0.6", 661 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 662 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" 663 | }, 664 | "debug": { 665 | "version": "2.6.9", 666 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 667 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 668 | "requires": { 669 | "ms": "2.0.0" 670 | } 671 | }, 672 | "depd": { 673 | "version": "2.0.0", 674 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 675 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 676 | }, 677 | "destroy": { 678 | "version": "1.2.0", 679 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 680 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" 681 | }, 682 | "ee-first": { 683 | "version": "1.1.1", 684 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 685 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 686 | }, 687 | "encodeurl": { 688 | "version": "1.0.2", 689 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 690 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" 691 | }, 692 | "escape-html": { 693 | "version": "1.0.3", 694 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 695 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 696 | }, 697 | "etag": { 698 | "version": "1.8.1", 699 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 700 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" 701 | }, 702 | "express": { 703 | "version": "4.18.1", 704 | "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", 705 | "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", 706 | "requires": { 707 | "accepts": "~1.3.8", 708 | "array-flatten": "1.1.1", 709 | "body-parser": "1.20.0", 710 | "content-disposition": "0.5.4", 711 | "content-type": "~1.0.4", 712 | "cookie": "0.5.0", 713 | "cookie-signature": "1.0.6", 714 | "debug": "2.6.9", 715 | "depd": "2.0.0", 716 | "encodeurl": "~1.0.2", 717 | "escape-html": "~1.0.3", 718 | "etag": "~1.8.1", 719 | "finalhandler": "1.2.0", 720 | "fresh": "0.5.2", 721 | "http-errors": "2.0.0", 722 | "merge-descriptors": "1.0.1", 723 | "methods": "~1.1.2", 724 | "on-finished": "2.4.1", 725 | "parseurl": "~1.3.3", 726 | "path-to-regexp": "0.1.7", 727 | "proxy-addr": "~2.0.7", 728 | "qs": "6.10.3", 729 | "range-parser": "~1.2.1", 730 | "safe-buffer": "5.2.1", 731 | "send": "0.18.0", 732 | "serve-static": "1.15.0", 733 | "setprototypeof": "1.2.0", 734 | "statuses": "2.0.1", 735 | "type-is": "~1.6.18", 736 | "utils-merge": "1.0.1", 737 | "vary": "~1.1.2" 738 | } 739 | }, 740 | "finalhandler": { 741 | "version": "1.2.0", 742 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", 743 | "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", 744 | "requires": { 745 | "debug": "2.6.9", 746 | "encodeurl": "~1.0.2", 747 | "escape-html": "~1.0.3", 748 | "on-finished": "2.4.1", 749 | "parseurl": "~1.3.3", 750 | "statuses": "2.0.1", 751 | "unpipe": "~1.0.0" 752 | } 753 | }, 754 | "forwarded": { 755 | "version": "0.2.0", 756 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 757 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" 758 | }, 759 | "fresh": { 760 | "version": "0.5.2", 761 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 762 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" 763 | }, 764 | "function-bind": { 765 | "version": "1.1.1", 766 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 767 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 768 | }, 769 | "get-intrinsic": { 770 | "version": "1.1.3", 771 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", 772 | "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", 773 | "requires": { 774 | "function-bind": "^1.1.1", 775 | "has": "^1.0.3", 776 | "has-symbols": "^1.0.3" 777 | } 778 | }, 779 | "has": { 780 | "version": "1.0.3", 781 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 782 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 783 | "requires": { 784 | "function-bind": "^1.1.1" 785 | } 786 | }, 787 | "has-symbols": { 788 | "version": "1.0.3", 789 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 790 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" 791 | }, 792 | "http-errors": { 793 | "version": "2.0.0", 794 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 795 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 796 | "requires": { 797 | "depd": "2.0.0", 798 | "inherits": "2.0.4", 799 | "setprototypeof": "1.2.0", 800 | "statuses": "2.0.1", 801 | "toidentifier": "1.0.1" 802 | } 803 | }, 804 | "iconv-lite": { 805 | "version": "0.4.24", 806 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 807 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 808 | "requires": { 809 | "safer-buffer": ">= 2.1.2 < 3" 810 | } 811 | }, 812 | "inherits": { 813 | "version": "2.0.4", 814 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 815 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 816 | }, 817 | "ipaddr.js": { 818 | "version": "1.9.1", 819 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 820 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 821 | }, 822 | "media-typer": { 823 | "version": "0.3.0", 824 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 825 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" 826 | }, 827 | "merge-descriptors": { 828 | "version": "1.0.1", 829 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 830 | "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" 831 | }, 832 | "methods": { 833 | "version": "1.1.2", 834 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 835 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" 836 | }, 837 | "mime": { 838 | "version": "1.6.0", 839 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 840 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 841 | }, 842 | "mime-db": { 843 | "version": "1.52.0", 844 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 845 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 846 | }, 847 | "mime-types": { 848 | "version": "2.1.35", 849 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 850 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 851 | "requires": { 852 | "mime-db": "1.52.0" 853 | } 854 | }, 855 | "ms": { 856 | "version": "2.0.0", 857 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 858 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 859 | }, 860 | "negotiator": { 861 | "version": "0.6.3", 862 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", 863 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" 864 | }, 865 | "object-inspect": { 866 | "version": "1.12.2", 867 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", 868 | "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" 869 | }, 870 | "on-finished": { 871 | "version": "2.4.1", 872 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 873 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 874 | "requires": { 875 | "ee-first": "1.1.1" 876 | } 877 | }, 878 | "parseurl": { 879 | "version": "1.3.3", 880 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 881 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 882 | }, 883 | "path-to-regexp": { 884 | "version": "0.1.7", 885 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 886 | "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" 887 | }, 888 | "proxy-addr": { 889 | "version": "2.0.7", 890 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 891 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 892 | "requires": { 893 | "forwarded": "0.2.0", 894 | "ipaddr.js": "1.9.1" 895 | } 896 | }, 897 | "qs": { 898 | "version": "6.10.3", 899 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", 900 | "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", 901 | "requires": { 902 | "side-channel": "^1.0.4" 903 | } 904 | }, 905 | "range-parser": { 906 | "version": "1.2.1", 907 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 908 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 909 | }, 910 | "raw-body": { 911 | "version": "2.5.1", 912 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", 913 | "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", 914 | "requires": { 915 | "bytes": "3.1.2", 916 | "http-errors": "2.0.0", 917 | "iconv-lite": "0.4.24", 918 | "unpipe": "1.0.0" 919 | } 920 | }, 921 | "safe-buffer": { 922 | "version": "5.2.1", 923 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 924 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 925 | }, 926 | "safer-buffer": { 927 | "version": "2.1.2", 928 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 929 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 930 | }, 931 | "send": { 932 | "version": "0.18.0", 933 | "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", 934 | "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", 935 | "requires": { 936 | "debug": "2.6.9", 937 | "depd": "2.0.0", 938 | "destroy": "1.2.0", 939 | "encodeurl": "~1.0.2", 940 | "escape-html": "~1.0.3", 941 | "etag": "~1.8.1", 942 | "fresh": "0.5.2", 943 | "http-errors": "2.0.0", 944 | "mime": "1.6.0", 945 | "ms": "2.1.3", 946 | "on-finished": "2.4.1", 947 | "range-parser": "~1.2.1", 948 | "statuses": "2.0.1" 949 | }, 950 | "dependencies": { 951 | "ms": { 952 | "version": "2.1.3", 953 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 954 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 955 | } 956 | } 957 | }, 958 | "serve-static": { 959 | "version": "1.15.0", 960 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", 961 | "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", 962 | "requires": { 963 | "encodeurl": "~1.0.2", 964 | "escape-html": "~1.0.3", 965 | "parseurl": "~1.3.3", 966 | "send": "0.18.0" 967 | } 968 | }, 969 | "setprototypeof": { 970 | "version": "1.2.0", 971 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 972 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 973 | }, 974 | "side-channel": { 975 | "version": "1.0.4", 976 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 977 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 978 | "requires": { 979 | "call-bind": "^1.0.0", 980 | "get-intrinsic": "^1.0.2", 981 | "object-inspect": "^1.9.0" 982 | } 983 | }, 984 | "statuses": { 985 | "version": "2.0.1", 986 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 987 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" 988 | }, 989 | "toidentifier": { 990 | "version": "1.0.1", 991 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 992 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 993 | }, 994 | "type-is": { 995 | "version": "1.6.18", 996 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 997 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 998 | "requires": { 999 | "media-typer": "0.3.0", 1000 | "mime-types": "~2.1.24" 1001 | } 1002 | }, 1003 | "unpipe": { 1004 | "version": "1.0.0", 1005 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1006 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" 1007 | }, 1008 | "utils-merge": { 1009 | "version": "1.0.1", 1010 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 1011 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" 1012 | }, 1013 | "vary": { 1014 | "version": "1.1.2", 1015 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1016 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" 1017 | } 1018 | } 1019 | } 1020 | -------------------------------------------------------------------------------- /part1/webservers/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-servers", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "scripts": { 7 | "hardcoded": "node src/hardcoded.js", 8 | "static": "node src/static.js", 9 | "dynamic": "node src/dynamic.js" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "body-parser": "^1.20.0", 16 | "express": "^4.18.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /part1/webservers/express/src/dynamic.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import body from "body-parser"; 3 | 4 | const PORT = 3000; 5 | const application = express(); 6 | 7 | const data = []; 8 | 9 | const template = (items) => ` 10 |
    11 | 12 | 13 | 14 |
    15 | 18 | `; 19 | 20 | application.use(body.urlencoded({ extended: false })); 21 | 22 | application.get("/", (request, response) => { 23 | response.send(template(data)); 24 | }); 25 | 26 | application.post("/", (request, response) => { 27 | data.push({ id: data.length + 1, name: request.body.item }); 28 | response.redirect("/"); 29 | }); 30 | 31 | application.listen(PORT, () => { 32 | console.log(`Listening on port ${PORT}`); 33 | }); 34 | -------------------------------------------------------------------------------- /part1/webservers/express/src/hardcoded.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | 3 | const PORT = 3000; 4 | const application = express(); 5 | 6 | const hardcoded = ` 7 | 8 | 9 | 10 | 11 | Basic Document 12 | 13 | 14 | 15 |

    Basic Document

    16 | 17 | 18 | 19 | `; 20 | 21 | application.get("/", (request, response) => { 22 | response.send(hardcoded); 23 | }); 24 | 25 | application.listen(PORT, () => { 26 | console.log(`Listening on port ${PORT}`); 27 | }); 28 | -------------------------------------------------------------------------------- /part1/webservers/express/src/static.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import { fileURLToPath } from "url"; 3 | 4 | const PORT = 3000; 5 | const application = express(); 6 | 7 | application.use(express.static("../public")); 8 | 9 | application.get("/", (request, response) => { 10 | return response.sendFile("index.html", { 11 | root: fileURLToPath(new URL("../../public", import.meta.url)), 12 | }); 13 | }); 14 | 15 | application.listen(PORT, () => { 16 | console.log(`Listening on port ${PORT}`); 17 | }); 18 | -------------------------------------------------------------------------------- /part1/webservers/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Basic Document 6 | 7 | 8 | 9 | 10 |

    Basic Document

    11 |

    12 | A linked document 13 |

    14 | 15 | 16 | -------------------------------------------------------------------------------- /part1/webservers/public/linked.html: -------------------------------------------------------------------------------- 1 |

    Linked Document

    2 |

    3 | Back 4 |

    -------------------------------------------------------------------------------- /part1/webservers/public/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Verdana", sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /part2/README.md: -------------------------------------------------------------------------------- 1 | # UI Architecture - Part Two 2 | 3 | ## Modern Experiences 4 | 5 | - Components 6 | - Managing State 7 | - Side Effects 8 | - Ecosystem 9 | 10 | ## 1. Complexity 11 | 12 | Architecture is a term that describes the process of making decisions that allow us to deliver the feature we need today while maintaining a level of changeability that enables us to adapt to our changing environment. We can use architecture to manage the complexity of a project. 13 | 14 | Good user experience is a competitive advantage for a business. The details of what we declare a “good” experience vary, but as developers, we recognize that a commitment to creating a good user experience can cause the code we write to increase in complexity. We must deliver an experience that matches growing user expectations. Modern web applications use JavaScript to enhance the experience in many ways. 15 | 16 | - Smooth transitions when interacting with the document. 17 | - Retention of user state between transitions. 18 | - Multiple methods of user interaction to trigger transitions. 19 | - Updates automatically with multi-user editing. 20 | 21 | It is common to use a library or framework to build web applications. Hotwire with Rails, React, Vue and Angular are all examples. React frameworks like Next.js and Remix provide a fast initial load with server-rendered HTML. These tools primarily want to make it easier to build an experience that matches what the user expects. 22 | 23 | React is popular. The State of JS survey from 2021 has it listed at 80%, with Angular being the next most popular at 54%. Many job postings include React as an experience requirement and popular libraries we use with React. 24 | 25 | Understanding React application architecture helps us build software that we can maintain in the long term. Breaking an application into components does not automatically provide a clean architecture. The decisions around storing state, handling side-effects and styling are what allow us to reduce bugs and spend more time adding new features. These seven goals can help during the decision-making process. 26 | 27 | ### Think in interfaces 28 | 29 | Specifically the interface of each component. The interface of a component is defined by the props we pass and the elements that the component returns. When designing the interface of a component we must understand the purpose of the component, and create a minimal interface to satisfy the requirements. 30 | 31 | ### Minimize duplicate JSX 32 | 33 | Component-based user interfaces benefit from reuse, we can watch for repetition in our JSX and decide how to divide the application into components. When we design these components we can use composition patterns and ensure that our conditions are localized to reduce the duplication of JSX. 34 | 35 | ### Minimize duplicate state 36 | 37 | React allows us to store state in any component that we create. When we store the same state in more than one location we need to ensure that the state remains in sync. We can avoid this challenge by ensuring that all state has a single origin. 38 | 39 | ### Prefer handlers for side-effects 40 | 41 | Side effects should be triggered through event handlers primarily. Sometimes we need to use effects to synchronize our components with external systems. 42 | 43 | ### Identify root dependencies 44 | 45 | We can pass state from parent to child as props. When integrating the components in our application we will combine the state from various sources to create new types of data. A clear understanding of the path from the data we display back to its root sources helps debug issues. 46 | 47 | ### List all dependencies, and reduce them if possible 48 | 49 | The dependencies of useMemo, useCallback and useEffect are determined by the code that they contain. We need to include all dependencies in the dependency array to avoid bugs related to stale closure. It can be easier to list all dependencies when they are few. 50 | 51 | ### Use context to share things that don't change 52 | 53 | The React context API can reduce the number of props that we need to pass down through many layers of a component tree. If we aren’t careful, creating many context providers can result in a poor developer experience. We want to minimize our use of context, when we do use it, we should use it for values that do not change often. The provider should be located at the lowest point of the tree that allows access from all the required consumers. 54 | 55 | ## 2. Components 56 | 57 | Components are responsible for returning React elements. A static site will always return the same tree each time we render it. Dynamic sites will cause mutations to the DOM based on the state change of state within the application. 58 | 59 | We should define categories of components that are associated with base responsibilities. This has an impact on the structure of a project and helps us limit the workload for any one component. 60 | 61 | - Shared-use UI controls 62 | - Single-use UI controls 63 | - Page containers 64 | - Context providers 65 | - Layout containers 66 | - Pure props 67 | 68 | Within the definition of our component, we decide the styles we apply want to apply to elements. There are a lot of ways to style React components, in this example we are providing a class name to elements. The `cx` function is using a helper called `classnames` to conditionally apply styles. 69 | 70 | ```jsx 71 | const NavigationItem = (props) => { 72 | const Icon = props.icon; 73 | 74 | return ( 75 |
  • 80 |   81 | 82 | 83 | 84 | {props.label} 85 |
  • 86 | ); 87 | }; 88 | ``` 89 | 90 | We also choose which elements we display with conditional rendering. We use this pattern to show errors, modals, loading icons and empty states. 91 | 92 | ```jsx 93 | export default function EntryList(props) { 94 | const entries = useEntries({ 95 | filter: { 96 | year: props.year, 97 | month: props.month, 98 | day: props.day, 99 | }, 100 | }); 101 | 102 | if (entries.length === 0) { 103 | return ; 104 | } 105 | 106 | return ( 107 | 112 | ); 113 | } 114 | ``` 115 | 116 | Any feature that needs to synchronize our components with external systems can use effects. We can provide keyboard based input by registering the necessary event handlers. 117 | 118 | ```jsx 119 | useEffect(() => { 120 | const handleKeyPress = (event: KeyboardEvent) => { 121 | if (event.key === "ArrowLeft") { 122 | onPrevious(); 123 | } 124 | 125 | if (event.key === "ArrowRight") { 126 | onNext(); 127 | } 128 | }; 129 | 130 | document.addEventListener("keyup", handleKeyPress); 131 | 132 | return () => { 133 | document.removeEventListener("keyup", handleKeyPress); 134 | }; 135 | }, [onPrevious, onNext]); 136 | ``` 137 | 138 | Often we will transform data from one type to another before we display it or pass it to a child component. This code exists as a custom hook to avoid having to repeat this code in each component that needs access to the same structure. 139 | 140 | ## 3. Managing State 141 | 142 | There is no single way to manage the state in a complex application. Depending on the purpose of the state, we might choose a different way to manage it. Our initial considerations determine if the state is required since we want only to store the minimal state necessary. If we require new state, we can categorize it to determine how to manage it. 143 | 144 | ### Server cache 145 | 146 | Persistent data mostly comes from a server. We must store the data we retrieve through an API within the application. We can store the data in a page container and then re-retrieve it each time the container is loaded. We can also store it higher in the tree, so it is available even when the component that requires it reloads. 147 | 148 | ### Local storage 149 | 150 | The browser can store data for a web application in the browser. The local storage API lets us set and get values using a key. Local storage is an external system; using an effect allows us to synchronize with React state. 151 | 152 | ### Session 153 | 154 | A successful login to a site can result in a response that sets a cookie in the browser. It is not common to attempt to read this session within the client application. Instead, we benefit from the automatic behaviour of sending the cookie back to the server with every subsequent request. 155 | 156 | ### Visual 157 | 158 | State that we use to apply conditional styles or conditionally render elements can live in. the closest common ancestor that needs it. 159 | 160 | ### Controlled input 161 | 162 | Input elements store state. When we need to synchronize the state of an input to a React component, we can use a controlled input pattern. We should store this state in the closest common ancestor of the components that need access. There are other form patterns that we can use to avoid controlled inputs. 163 | 164 | ### Input validation 165 | 166 | The validation of user input may result in the need to display an error message. Client-side validation is purely a user experience consideration. The server will need to validate inputs before performing a mutation. 167 | 168 | ### Async operation 169 | 170 | We can combine the state used to track a network request's loading, data and error state. The loading state is set to true when we start the operation. If the operation is successful, we set the data. If the operation results in an error, then we set the error state. 171 | 172 | ### Navigation 173 | 174 | The URL is one of the oldest locations to store state for web applications. Instead of the server parsing the URL for params, we can perform this same operation in our web client. We can conditionally render components based on the current location and use params to make our components display resource-specific data. 175 | 176 | ## 4. Side Effects 177 | 178 | React Hooks have been publicly available for over three years. Since React 16.8, avoiding bad advice on using the “useEffect” Hook has been challenging. Educators that intend to help developers learn how to use Hooks can often provide a minimal explanation that is simple but wrong. 179 | 180 | > 🚫 “Leave the dependency array empty if you only want to run the effect on mount.” 181 | 182 | This advice is a simple way to think about it, but what if there are dependencies? It is the contents of “useEffect” that define the dependencies. We cannot remove a dependency by leaving it out of our dependency array. Doing so creates stale closure defects that can be hard to debug. Ignoring the eslint error for the “exhaustive-deps” rule prevents any further reporting that may be valid. With React 18, all components will mount, unmount and mount again when we enable Strict Mode. 183 | 184 | Instead of taking the simple but wrong path, we can learn the techniques that allow us to include all our dependencies. If we want to avoid the infinite render loops, we need to understand referential stability. 185 | 186 | ```jsx 187 | function UserProfile() { 188 | const [user, setUser] = useState(null); 189 | 190 | const getUser = () => 191 | fetch("/api/user/me") 192 | .then((response) => response.json()) 193 | .then((user) => setUser(user)); 194 | 195 | useEffect(() => { 196 | getUser(); 197 | }, []); // getUser is missing from the array 198 | 199 | return user &&
    {user.name}
    ; 200 | } 201 | ``` 202 | 203 | A common technique for fetching data requires a `useEffect` with a call to fetch data and then set the data as React state when the response is successful. Calling a helper function that we declare in our component from a `useEffect` creates a dependency on an object that does not have a stable reference. 204 | 205 | If we decide that we want to show user information for any user with an`id` we can change the request to include a param. When the component mounts, it will load data for the user with the value defined by `props.id`. If the value of `props.id` changes; this code will not run to retrieve data for the user with the `id`. 206 | 207 | ```jsx 208 | function UserProfile(props) { 209 | const [user, setUser] = useState(null); 210 | 211 | const { id } = props; 212 | 213 | const getUser = () => 214 | fetch(`/api/user/${id}`) 215 | .then((response) => response.json()) 216 | .then((user) => setUser(user)); 217 | 218 | useEffect(() => { 219 | getUser(); 220 | }, []); 221 | 222 | return user &&
    {user.name}
    ; 223 | } 224 | ``` 225 | 226 | When we have a dependency, we include it in the array. To reduce our dependencies, we can move the helper function into the `useEffect` or remove the helper function wrapper altogether. We do not need to declare `setUser` as a dependency because it is guaranteed stable. If the value of `props.id` changes, this code will run to retrieve the data for the user with the `id`. 227 | 228 | ```jsx 229 | function UserProfile(props) { 230 | const [user, setUser] = useState(null); 231 | 232 | const { id } = props; 233 | 234 | useEffect(() => { 235 | fetch(`/api/user/${id}`) 236 | .then((response) => response.json()) 237 | .then((user) => setUser(user)); 238 | }, [id]); 239 | 240 | return user &&
    {user.name}
    ; 241 | } 242 | ``` 243 | 244 | The dependency array becomes complicated when we start passing objects to children as props. When a component React renders a component, the variables created during the calling of the function will not maintain the same reference from a previous render. A new object could have the same shape and data, but they will not be equal if we compare it to the last object by reference. 245 | 246 | ```jsx 247 | const { get, set } = useCache(key); 248 | 249 | useEffect(() => { 250 | let ignore = false; 251 | 252 | request().then((data) => { 253 | if (ignore === false) { 254 | set(data); 255 | } 256 | }); 257 | 258 | return () => { 259 | ignore = true; 260 | }; 261 | }, [request, set]); 262 | ``` 263 | 264 | The `useCache` Hook creates the `set` function whenever a component calls it. When we create a dependency on a function, like `set`, we need to ensure that any closure `set` has is updated with the latest values. If the act of rendering causes us to recreate the `set` function without any of its root dependencies changing, then we can cause an infinite loop. 265 | 266 | We can use the `useCallback` Hook to ensure that we only create a new reference for the `set` function when any dependencies change. 267 | 268 | ```jsx 269 | const set = useCallback( 270 | (value) => setCache((cache) => ({ ...cache, [key]: value })), 271 | [key, setCache] 272 | ); 273 | ``` 274 | 275 | When a state mutation depends on the state's existing value, we can run into more common problems. When we use the reducer form of `setCache` we avoid creating a dependency on the existing `cache` state. This technique is one of the ways that we can reduce our dependencies. If we don't use the reducer, the `set` function has a new reference each time we update the cache. 276 | 277 | ```jsx 278 | const set = useCallback( 279 | (value) => setCache({ ...cache, [key]: value }), 280 | [key, cache, setCache] 281 | ); 282 | ``` 283 | 284 | This reference instability can cascade to child components, causing any effect that depends on `set` to run in a loop without further protection. In this next example, we only request if the data value is currently null. The dependency array includes all of the dependencies for this effect. 285 | 286 | ```jsx 287 | useEffect(() => { 288 | if (data !== null) return; 289 | 290 | let ignore = false; 291 | 292 | request().then((data) => { 293 | if (ignore === false) { 294 | set(data); 295 | } 296 | }); 297 | 298 | return () => { 299 | ignore = true; 300 | }; 301 | }, [request, data, set]); 302 | ``` 303 | 304 | Instead of removing a dependency that belongs in the array, try one of the following approaches to reduce dependencies and control referential stability. 305 | 306 | - Move the function into the effect, or remove the wrapper. 307 | - Use the useCallback Hook with a full dependency list. 308 | - Use a reducer function with the set state actions. 309 | - Execute effects conditionally within the effect function. 310 | - Split up effects into multiple useEffect hooks. 311 | 312 | With the correct dependency list in place, we can ensure that we return a function from our effect, that allows for cleanup. We can use this to unsubscribe from events, clear timers or ignore the response of an HTTP request. 313 | 314 | ## 5. Ecosystem 315 | 316 | Many packages exist that provide abstractions for React applications. We can learn a lot from these packages, even if we choose to use a limited number of them. 317 | 318 | ### State Management 319 | 320 | [https://redux-toolkit.js.org/](https://redux-toolkit.js.org/) 321 | 322 | [https://github.com/pmndrs/zustand](https://zustand-demo.pmnd.rs/) 323 | 324 | [https://mobx.js.org/](https://mobx.js.org/README.html) 325 | 326 | [https://recoiljs.org/](https://recoiljs.org/) 327 | 328 | [https://jotai.org/](https://jotai.org/) 329 | 330 | [https://xstate.js.org/](https://xstate.js.org/) 331 | 332 | ### Routing 333 | 334 | [https://reactrouter.com/](https://reactrouter.com/) 335 | 336 | [https://react-location.tanstack.com/](https://react-location.tanstack.com/) 337 | 338 | ### Server Cache Management 339 | 340 | [https://react-query.tanstack.com/](https://react-query.tanstack.com/) 341 | 342 | [https://www.apollographql.com/docs/react/](https://www.apollographql.com/docs/react/) 343 | 344 | [https://relay.dev/](https://relay.dev/) 345 | 346 | [https://swr.vercel.app/](https://swr.vercel.app/) 347 | 348 | [https://formidable.com/open-source/urql/](https://formidable.com/open-source/urql/) 349 | 350 | ### Forms 351 | 352 | [https://formik.org/](https://formik.org/) 353 | 354 | [https://react-hook-form.com/](https://react-hook-form.com/) 355 | 356 | ### Frameworks 357 | 358 | [https://nextjs.org/](https://nextjs.org/) 359 | 360 | [https://remix.run/](https://remix.run/) 361 | -------------------------------------------------------------------------------- /part3/README.md: -------------------------------------------------------------------------------- 1 | # UI Architecture - Part Three 2 | 3 | ## Style 4 | 5 | - Abstraction 6 | - CSS 7 | - Tooling 8 | - Tailwind 9 | - CSS-in-JS 10 | 11 | 12 | ## 1. Abstraction 13 | 14 | Building web interfaces is complicated. Challenges with layout and styling can be daunting when a project reaches a larger scale. These challenges have driven many in the web development community to build abstractions that ease the process. Various CSS frameworks, component libraries and toolchains provide a strong foundation for modern approaches to styling. On occasion, new CSS specifications include features first available from third-party providers. 15 | 16 | Our ability to share libraries full of components leads to an incredible increase in productivity. Since good design follows patterns, most libraries implement the same types of components. It is nice not to write all the code for a button with different variants or a complex table component, but this does not include all the benefits. 17 | 18 | A design system is what provides structure to a project. It takes less time to build when we already have the answer to most questions surrounding look and feel. The component libraries worth using are all based on a design system. The following are reasonable choices for a new project [Material UI](https://mui.com/), [Bootstrap](https://react-bootstrap.github.io/), [Polaris](https://polaris.shopify.com/components), [Primer](https://primer.style/react/), [Chakra UI](https://chakra-ui.com/), [Fluent UI](https://developer.microsoft.com/en-us/fluentui), [Blueprint](https://blueprintjs.com/), [Rebass](https://rebassjs.org/), [BaseWeb](https://baseweb.design/), [Garden](https://garden.zendesk.com/), [ADS](https://atlassian.design/), [Protocol](https://protocol.mozilla.org/), [Lightning](https://www.lightningdesignsystem.com/), [Cedar](https://rei.github.io/rei-cedar-docs/), [Carbon](https://carbondesignsystem.com/), [Elastic UI](https://elastic.github.io/eui/), [Theme UI](https://theme-ui.com/), and [Mantine](https://mantine.dev/). 19 | 20 | It is not typical to mix these libraries; a project should contain at most one of these dependencies. We can break this rule if a project is actively moving from one UI library to another. To choose a library, we can follow a process where we identify the components we need; once we have filtered the list to include libraries that contain the components we need, we determine which approach each one uses for styling. 21 | 22 | Sometimes it is enough to choose a library that looks good enough to ship as is, especially for internal projects where branding isn’t necessary. When we are building a project that requires a specific visual design, the most critical part of the UI library to understand is the mechanism we can use to change how it looks. If we design a project using an existing design system as a basis, it is easier to configure our theme to match our target look. 23 | 24 | UI libraries are expensive dependencies; they offer a lot of benefits when getting started but can present challenges when we are pushed outside of their constraints. In some cases, teams may decide to implement a custom library of components for their project. Using any approach found in popular UI libraries would provide a good foundation. 25 | 26 | Lately, utility-first CSS frameworks have become popular, with [Tailwind CSS](https://tailwindcss.com/) leading the category. These frameworks don’t provide components and instead re-present CSS properties as classes that can be composed using the `class` attribute of an HTML element. [Windi CSS](https://windicss.org/), [Tachyons](http://tachyons.io/), [Basscss](https://basscss.com/) and [Master CSS](https://css.master.co/) frameworks also come with a built-in design system that we can customize. 27 | 28 | One of the significant differences between a UI library and a CSS framework is the separation of concerns. A UI library tends to bundle how the component looks and how it behaves as a single consideration. CSS frameworks focus on providing tools to implement the visual design of components within an interface quickly, but they do not take any responsibility for functionality. 29 | 30 | In general, we can build components that do not have an opinion on how they look, the term used to categorize these types of components is Headless UI. Libraries like [Downshift](https://www.downshift-js.com/) and [TanStack Table](https://tanstack.com/table/v8) embrace this by providing accessible components we can style using any technique. 31 | 32 | [Headless UI](https://headlessui.com/), [Radix](https://www.radix-ui.com/), [React Aria](https://react-spectrum.adobe.com/react-aria/), [Reakit](https://reakit.io/) and [Ariakit](https://ariakit.org/) are all built with this goal in mind. We can compose CSS frameworks and Headless UI libraries to create accessible components that follow a design system. The Headless UI library pairs well with TailwindCSS, while Radix and [Stitches](https://stitches.dev/) provide suitable alternatives to fill the same role. 33 | 34 | These abstractions help us build interfaces faster, but they do not allow us to skip understanding the platform-supported features. Avoid buying site templates unless all technologies are well understood; they can cause more confusion than building on a proven design system. 35 | 36 | ## 2. CSS 37 | 38 | Building a design system for a large project using plain CSS is a significant undertaking. Unsurprisingly, smaller teams will reach for an existing solution to save development time. It is still important to understand how the platform works so that we can use the Chrome DevTools when something goes wrong. 39 | 40 | CSS allows us to alter how our document looks when it renders. We can break this responsibility into two sub-categories; Layout and Styling. Styling is more straightforward since it mainly involves altering the properties of our typography, including the size and colour. Originally the browser was designed to render documents, not applications. 41 | 42 | CSS has added numerous features since its inception to provide tools for layout. With the box model, the initial layout uses static positioning. We can make an element positioned using the values `relative` or `absolute`. Elements we position using `absolute` also fall out of the normal flow of content. We can use this to stack elements in our UI. 43 | 44 | Positioning elements based on screen coordinates isn’t very responsive, so we must be cautious when using this approach. We often use Flexbox, Grid, or a combination of the two when addressing our layout. We should lock our spacing values to a scale and apply padding or margins appropriately. 45 | 46 | A colour palette for a project is usually quite limited; it is a good practice to declare the colour values as CSS variables using semantic names. We can use Lint to ensure that all rules reference variables instead of hard-coded colour values. 47 | 48 | ## 3. Tooling 49 | 50 | Sass has been around for fifteen years; it provides numerous benefits to those who write large-scale CSS systems. Over time the CSS specification has evolved and now includes features previously only available with third-party tooling. As the platform grows, we should change our approach if it serves us well. For example, now that they are available when we are targeting modern browsers, we can use built-in CSS variables. 51 | 52 | The introduction of tooling to the CSS development process allows for incredible flexibility in how we author our CSS. As UI developers, we face challenges with organizing our system due to common issues such as name clashing, specificity overruling and browser compatibility. Tools like PostCSS help us by providing necessary features. 53 | 54 | We can reduce the complexity of CSS with features like automatic property prefixing, scoping class names and linting. We configure our tools to allow for non-standard features like nesting. 55 | 56 | ## 4. Tailwind 57 | 58 | Before CSS, it was possible to style text using the `` element with `color` or `size` attributes. When we embed our style with our structure, we seek ways to separate it as we scale. We don’t often recommend inline styles or embedded style tags for anything other than testing stuff out, even though it is pretty convenient to co-locate styles with our HTML. 59 | 60 | Advocates base their argument for using Tailwind CSS on the idea that naming things is hard. If we don’t have to come up with class names, we can quickly iterate on our designs. Naming is hard, but it is not the problem we avoid when using Tailwind CSS. Eventually, we will have to name something; avoiding it is not a solution. 61 | 62 | We can test Tailwind's highly configurable design system on their [Playground](https://play.tailwindcss.com/). We fill our markup with utility class names without tools like React to abstract components into smaller pieces. As our lists grow in length, we can benefit by using an auto-sorting prettier rule to ensure our class names have some level of organization. 63 | 64 | Tailwind Labs offers paid component templates called [Tailwind UI](https://tailwindui.com/) which can reduce the time launching a typical site. It has also inspired complete open source UI libraries like [daisyUI](https://daisyui.com/) and CSS-in-JS runtimes like [Twind](https://twind.dev/). 65 | 66 | ## 5. CSS-in-JS 67 | 68 | CSS-in-JS libraries provide the basis for most of the popular UI component libraries. If we use a UI component library that depends on Emotion, it is probably a good idea to use Emotion for our custom components. When we do this, we gain several benefits. 69 | 70 | The most obvious difference is that we can write our CSS directly in our component source files. These tools assign unique CSS class names to avoid a naming collision, and vendor prefixes are added to the resulting output to improve compatibility. Other benefits include good compatibility with SSR and theming support. 71 | 72 | When choosing a library, we should consider the style definition syntax, the style application syntax and the output format. The output format is usually a separate `.css` file or a runtime that creates one or more `