diff --git a/exercises/07.client-hydration/01.solution.bootstrap/.gitignore b/exercises/07.client-hydration/01.solution.bootstrap/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/07.client-hydration/01.solution.bootstrap/.prettierignore b/exercises/07.client-hydration/01.solution.bootstrap/.prettierignore new file mode 100644 index 0000000..4dd9aa1 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/.prettierignore @@ -0,0 +1,3 @@ +node_modules +package-lock.json +built_node_modules diff --git a/exercises/07.client-hydration/01.solution.bootstrap/.prettierrc b/exercises/07.client-hydration/01.solution.bootstrap/.prettierrc new file mode 100644 index 0000000..da3a81a --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/.prettierrc @@ -0,0 +1,28 @@ +{ + "arrowParens": "avoid", + "bracketSameLine": false, + "bracketSpacing": true, + "embeddedLanguageFormatting": "auto", + "endOfLine": "lf", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxSingleQuote": false, + "printWidth": 80, + "proseWrap": "always", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": false, + "singleAttributePerLine": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "all", + "useTabs": true, + "overrides": [ + { + "files": ["**/*.json"], + "options": { + "useTabs": false + } + } + ] +} diff --git a/exercises/07.client-hydration/01.solution.bootstrap/README.mdx b/exercises/07.client-hydration/01.solution.bootstrap/README.mdx new file mode 100644 index 0000000..14600f4 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/README.mdx @@ -0,0 +1 @@ +# Bootstrap Modules diff --git a/exercises/07.client-hydration/01.solution.bootstrap/db/ship-api.js b/exercises/07.client-hydration/01.solution.bootstrap/db/ship-api.js new file mode 100644 index 0000000..4f56d2f --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/db/ship-api.js @@ -0,0 +1,47 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +export async function searchShips({ + search, + delay = Math.random() * 200 + 300, +}) { + const endTime = Date.now() + delay + const ships = shipData + .filter(ship => ship.name.toLowerCase().includes(search.toLowerCase())) + .slice(0, 13) + await new Promise(resolve => setTimeout(resolve, endTime - Date.now())) + return { + ships: ships.map(ship => ({ name: ship.name, id: ship.id })), + } +} + +export async function getShip({ shipId, delay = Math.random() * 200 + 300 }) { + const endTime = Date.now() + delay + if (!shipId) { + throw new Error('No shipId provided') + } + const ship = shipData.find(ship => ship.id === shipId) + await new Promise(resolve => setTimeout(resolve, endTime - Date.now())) + if (!ship) { + throw new Error(`No ship with the id "${shipId}"`) + } + return ship +} + +export async function updateShipName({ + shipId, + shipName, + delay = Math.random() * 200 + 300, +}) { + const endTime = Date.now() + delay + const ship = shipData.find(ship => ship.id === shipId) + await new Promise(resolve => setTimeout(resolve, endTime - Date.now())) + if (!ship) { + throw new Error(`No ship with the id "${shipId}"`) + } + ship.name = shipName + return ship +} diff --git a/exercises/07.client-hydration/01.solution.bootstrap/db/ships.json b/exercises/07.client-hydration/01.solution.bootstrap/db/ships.json new file mode 100644 index 0000000..99a462f --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/db/ships.json @@ -0,0 +1,309 @@ +[ + { + "id": "bc4cbadf89bd3", + "name": "Infinity Drifter", + "image": "/ships/bc4cbadf89bd3.webp", + "topSpeed": 10, + "hyperdrive": true, + "weapons": [ + { + "name": "Laser", + "type": "beam", + "damage": 35 + }, + { + "name": "Photon Torpedo", + "type": "projectile", + "damage": 50 + }, + { + "name": "Plasma Cannon", + "type": "beam", + "damage": 75 + } + ] + }, + { + "id": "3ba8aa65ffe6c", + "name": "Star Hopper", + "image": "/ships/3ba8aa65ffe6c.webp", + "topSpeed": 8, + "hyperdrive": true, + "weapons": [ + { + "name": "Laser", + "type": "beam", + "damage": 25 + }, + { + "name": "Photon Torpedo", + "type": "projectile", + "damage": 40 + } + ] + }, + { + "id": "ab267a5984523", + "name": "Galaxy Cruiser", + "image": "/ships/ab267a5984523.webp", + "topSpeed": 6, + "hyperdrive": true, + "weapons": [ + { + "name": "Laser", + "type": "beam", + "damage": 15 + } + ] + }, + { + "id": "d3b8aa65ffe6c", + "name": "Planet Hopper", + "image": "/ships/d3b8aa65ffe6c.webp", + "topSpeed": 4, + "hyperdrive": false, + "weapons": [ + { + "name": "Laser", + "type": "beam", + "damage": 10 + } + ] + }, + { + "id": "1ff1991efe029", + "name": "Space Taxi", + "image": "/ships/1ff1991efe029.webp", + "topSpeed": 2, + "hyperdrive": false, + "weapons": [] + }, + { + "id": "f3d9a88e1c234", + "name": "Star Destroyer", + "image": "/ships/f3d9a88e1c234.webp", + "topSpeed": 12, + "hyperdrive": true, + "weapons": [ + { + "name": "Ion Cannon", + "type": "beam", + "damage": 60 + }, + { + "name": "Proton Torpedo", + "type": "projectile", + "damage": 80 + }, + { + "name": "Plasma Cannon", + "type": "beam", + "damage": 100 + } + ] + }, + { + "id": "cb03cc4e5717e", + "name": "Interceptor", + "image": "/ships/cb03cc4e5717e.webp", + "topSpeed": 9, + "hyperdrive": true, + "weapons": [ + { + "name": "Railgun", + "type": "projectile", + "damage": 45 + }, + { + "name": "EMP Blaster", + "type": "beam", + "damage": 70 + } + ] + }, + { + "id": "6c86fca8b9086", + "name": "Stealth Cruiser", + "image": "/ships/6c86fca8b9086.webp", + "topSpeed": 7, + "hyperdrive": true, + "weapons": [ + { + "name": "Cloaking Device", + "type": "special", + "damage": 0 + }, + { + "name": "Plasma Cannon", + "type": "beam", + "damage": 85 + } + ] + }, + { + "id": "fdc13cb488bf1", + "name": "Battleship", + "image": "/ships/fdc13cb488bf1.webp", + "topSpeed": 10, + "hyperdrive": false, + "weapons": [ + { + "name": "Cannon", + "type": "projectile", + "damage": 50 + }, + { + "name": "Missile Launcher", + "type": "projectile", + "damage": 70 + } + ] + }, + { + "id": "d486d48b82b81", + "name": "Dreadnought", + "image": "/ships/d486d48b82b81.webp", + "topSpeed": 8, + "hyperdrive": true, + "weapons": [ + { + "name": "Plasma Cannon", + "type": "beam", + "damage": 90 + }, + { + "name": "Quantum Torpedo", + "type": "projectile", + "damage": 120 + } + ] + }, + { + "id": "cfd10fcd2de6c", + "name": "Cruiser", + "image": "/ships/cfd10fcd2de6c.webp", + "topSpeed": 6, + "hyperdrive": false, + "weapons": [ + { + "name": "Laser Cannon", + "type": "beam", + "damage": 55 + }, + { + "name": "Missile Launcher", + "type": "projectile", + "damage": 75 + } + ] + }, + { + "id": "e92cefe4f6727", + "name": "Frigate", + "image": "/ships/e92cefe4f6727.webp", + "topSpeed": 5, + "hyperdrive": false, + "weapons": [ + { + "name": "Plasma Cannon", + "type": "beam", + "damage": 70 + }, + { + "name": "Torpedo Launcher", + "type": "projectile", + "damage": 60 + } + ] + }, + { + "id": "ec7a3f950f99f", + "name": "Scout Ship", + "image": "/ships/ec7a3f950f99f.webp", + "topSpeed": 11, + "hyperdrive": true, + "weapons": [] + }, + { + "id": "5c13d8b28a14a", + "name": "Bomber", + "image": "/ships/5c13d8b28a14a.webp", + "topSpeed": 8, + "hyperdrive": false, + "weapons": [ + { + "name": "Bomb Dropper", + "type": "projectile", + "damage": 90 + } + ] + }, + { + "id": "670003aed3795", + "name": "Transport Ship", + "image": "/ships/670003aed3795.webp", + "topSpeed": 4, + "hyperdrive": true, + "weapons": [] + }, + { + "id": "b442531ea32b2", + "name": "Gunship", + "image": "/ships/b442531ea32b2.webp", + "topSpeed": 7, + "hyperdrive": true, + "weapons": [ + { + "name": "Laser Cannon", + "type": "beam", + "damage": 65 + } + ] + }, + { + "id": "6f375578ead88", + "name": "Diplomatic Vessel", + "image": "/ships/6f375578ead88.webp", + "topSpeed": 3, + "hyperdrive": true, + "weapons": [ + { + "name": "Laser", + "type": "beam", + "damage": 5 + } + ] + }, + { + "id": "627c497212456", + "name": "Mining Ship", + "image": "/ships/627c497212456.webp", + "topSpeed": 4, + "hyperdrive": false, + "weapons": [] + }, + { + "id": "441f7092a8d44", + "name": "Research Vessel", + "image": "/ships/441f7092a8d44.webp", + "topSpeed": 3, + "hyperdrive": true, + "weapons": [] + }, + { + "id": "0268fc4817ad1", + "name": "Medical Ship", + "image": "/ships/0268fc4817ad1.webp", + "topSpeed": 2, + "hyperdrive": true, + "weapons": [] + }, + { + "id": "1ae7b4b92036b", + "name": "Cargo Ship", + "image": "/ships/1ae7b4b92036b.webp", + "topSpeed": 2, + "hyperdrive": true, + "weapons": [] + } +] diff --git a/exercises/07.client-hydration/01.solution.bootstrap/dev.js b/exercises/07.client-hydration/01.solution.bootstrap/dev.js new file mode 100644 index 0000000..b10826e --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/dev.js @@ -0,0 +1,58 @@ +import { spawn } from 'node:child_process' +import chalk from 'chalk' +import closeWithGrace from 'close-with-grace' +import getPort, { portNumbers } from 'get-port' + +function spawnScript(command, args, env, prefix) { + const script = spawn(command, args, { + env: { ...process.env, ...env }, + }) + + script.stdout.on('data', data => { + process.stdout.write(`[${prefix}] ${data}`) + }) + script.stderr.on('data', data => { + process.stderr.write(`[${prefix} ${chalk.red.bgBlack('ERROR')}] ${data}`) + }) + script.on('exit', code => { + process.stdout.write( + `[${prefix} ${chalk.yellow.bgBlack('exit')}]: ${code}\n`, + ) + }) + + return script +} + +const SSR_PORT = await getPort({ port: Number(process.env.PORT || 3000) }) +const RSC_PORT = await getPort({ port: portNumbers(9000, 9999) }) + +const ssrServer = spawnScript( + 'node', + ['--watch', 'server/ssr.js'], + { PORT: SSR_PORT, RSC_PORT }, + chalk.blue.bgBlack('SSR'), +) + +const rscServer = spawnScript( + 'node', + [ + '--watch', + '--import', + './server/register-rsc-loader.js', + '--conditions=react-server', + 'server/rsc.js', + ], + { PORT: RSC_PORT }, + chalk.green.bgBlack('RSC'), +) + +closeWithGrace(async () => { + console.log('Shutting down servers...') + const ssrExit = new Promise(resolve => ssrServer.on('exit', resolve)) + const rscExit = new Promise(resolve => rscServer.on('exit', resolve)) + + ssrServer.kill('SIGTERM') + rscServer.kill('SIGTERM') + + await Promise.all([ssrExit, rscExit]) +}) diff --git a/exercises/07.client-hydration/01.solution.bootstrap/package-lock.json b/exercises/07.client-hydration/01.solution.bootstrap/package-lock.json new file mode 100644 index 0000000..babe051 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/package-lock.json @@ -0,0 +1,1298 @@ +{ + "name": "super-simple-rsc", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "super-simple-rsc", + "version": "1.0.0", + "license": "MIT", + "workspaces": [ + "built_node_modules/*" + ], + "dependencies": { + "body-parser": "^1.20.2", + "busboy": "^1.6.0", + "chalk": "^5.3.0", + "compression": "^1.7.4", + "concurrently": "^8.2.2", + "cross-env": "^7.0.3", + "express": "^4.19.1", + "get-port": "^7.1.0", + "react": "0.0.0-experimental-2b036d3f1-20240327", + "react-dom": "0.0.0-experimental-2b036d3f1-20240327", + "react-error-boundary": "^4.0.13" + }, + "devDependencies": { + "prettier": "^3.2.5" + } + }, + "built_node_modules/react-server-dom-esm": { + "version": "0.0.0-experimental-5c65b2758-20240322", + "license": "MIT", + "dependencies": { + "acorn-loose": "^8.3.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "0.0.0-experimental-5c65b2758-20240322", + "react-dom": "0.0.0-experimental-5c65b2758-20240322" + } + }, + "node_modules/@babel/runtime": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", + "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-loose": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz", + "integrity": "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concurrently": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", + "integrity": "sha512-1dP4gpXFhei8IOtlXRE/T/4H88ElHgTiUzh71YUmtjTEHMSRS2Z/fgOxHSxxusGHogsRfxNq1vyAwxSC+EVyDg==", + "dependencies": { + "chalk": "^4.1.2", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", + "rxjs": "^7.8.1", + "shell-quote": "^1.8.1", + "spawn-command": "0.0.2", + "supports-color": "^8.1.1", + "tree-kill": "^1.2.2", + "yargs": "^17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": "^14.13.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.1.tgz", + "integrity": "sha512-K4w1/Bp7y8iSiVObmCrtq8Cs79XjJc/RU2YYkZQ7wpUu5ZyZ7MtPHkqoMz4pf+mgXfNvo2qft8D9OnrH2ABk9w==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-port": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-7.1.0.tgz", + "integrity": "sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", + "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "0.0.0-experimental-2b036d3f1-20240327", + "resolved": "https://registry.npmjs.org/react/-/react-0.0.0-experimental-2b036d3f1-20240327.tgz", + "integrity": "sha512-voyNichNzOf7n+hZYDg7snxYWukBr088SkfQ0jTBIoyWlYRR4MCY/ty/Z5yX2IS+SzyCwgHcuF1yYU3BOpAZvQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "0.0.0-experimental-2b036d3f1-20240327", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-0.0.0-experimental-2b036d3f1-20240327.tgz", + "integrity": "sha512-PLRQJr2Cr34vDjKdNKWP4YAUA0vhUfN1jXvuhiOurpDA6RIINANjDPBpQoASA1Eha/g4Zkey0aDi7rNWOmSqRA==", + "dependencies": { + "scheduler": "0.0.0-experimental-2b036d3f1-20240327" + }, + "peerDependencies": { + "react": "0.0.0-experimental-2b036d3f1-20240327" + } + }, + "node_modules/react-error-boundary": { + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.13.tgz", + "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/react-server-dom-esm": { + "resolved": "built_node_modules/react-server-dom-esm", + "link": true + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/scheduler": { + "version": "0.0.0-experimental-2b036d3f1-20240327", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.0.0-experimental-2b036d3f1-20240327.tgz", + "integrity": "sha512-AfEw/Icx++GJcnRpgbWeOTMmhrfOJqW8cewhzuL26wERWUjpyHI22NGetHSwu6xFU/9GuDfgrV5B308Fyxbk7w==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", + "dependencies": { + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", + "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/spawn-command": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", + "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/exercises/07.client-hydration/01.solution.bootstrap/package.json b/exercises/07.client-hydration/01.solution.bootstrap/package.json new file mode 100644 index 0000000..2e2cbfb --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/package.json @@ -0,0 +1,34 @@ +{ + "name": "exercises__sep__07.client-hydration__sep__01.solution.bootstrap", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node dev.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "body-parser": "^1.20.2", + "busboy": "^1.6.0", + "chalk": "^5.3.0", + "close-with-grace": "^1.3.0", + "compression": "^1.7.4", + "express": "^4.19.1", + "get-port": "^7.1.0", + "react": "0.0.0-experimental-2b036d3f1-20240327", + "react-dom": "0.0.0-experimental-2b036d3f1-20240327", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@0.0.0-experimental-2b036d3f1-20240327" + }, + "devDependencies": { + "@types/node": "^20.11.30", + "prettier": "^3.2.5" + }, + "eslintIgnore": [ + "node_modules" + ] +} diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/favicon.ico b/exercises/07.client-hydration/01.solution.bootstrap/public/favicon.ico new file mode 100644 index 0000000..890afb6 Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/favicon.ico differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/favicon.svg b/exercises/07.client-hydration/01.solution.bootstrap/public/favicon.svg new file mode 100644 index 0000000..67401c5 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/broken-ship.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/broken-ship.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/fallback-ship.png b/exercises/07.client-hydration/01.solution.bootstrap/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/fallback-ship.png differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/0268fc4817ad1.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/1ae7b4b92036b.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/1ff1991efe029.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/3ba8aa65ffe6c.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/441f7092a8d44.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/5c13d8b28a14a.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/627c497212456.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/627c497212456.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/670003aed3795.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/670003aed3795.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/6c86fca8b9086.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/6f375578ead88.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/ab267a5984523.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/b442531ea32b2.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/bc4cbadf89bd3.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/cb03cc4e5717e.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/cfd10fcd2de6c.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/d3b8aa65ffe6c.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/d486d48b82b81.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/e92cefe4f6727.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/ec7a3f950f99f.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/f3d9a88e1c234.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/fdc13cb488bf1.webp b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/07.client-hydration/01.solution.bootstrap/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/07.client-hydration/01.solution.bootstrap/public/style.css b/exercises/07.client-hydration/01.solution.bootstrap/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/public/style.css @@ -0,0 +1,157 @@ +html { + font-family: ui-sans-serif, system-ui; +} + +body { + margin: 0; +} + +* { + box-sizing: border-box; +} + +.app-wrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; +} + +.app { + display: flex; + max-width: 1024px; + border: 1px solid #000; + border-start-end-radius: 0.5rem; + border-start-start-radius: 0.5rem; + border-end-start-radius: 50% 8%; + border-end-end-radius: 50% 8%; + overflow: hidden; +} + +.search { + width: 150px; + max-height: 400px; + overflow: hidden; + display: flex; + flex-direction: column; + + input { + width: 100%; + border: 0; + border-bottom: 1px solid #000; + padding: 8px; + line-height: 1.5; + border-top-left-radius: 0.5rem; + } + + ul { + flex: 1; + list-style: none; + padding: 4px; + padding-bottom: 30px; + margin: 0; + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; + li { + a { + font-size: 0.8rem; + text-decoration: none; + color: black; + padding: 1px 6px; + display: flex; + align-items: center; + gap: 4px; + border: none; + background-color: transparent; + &:hover { + text-decoration: underline; + } + img { + width: 20px; + height: 20px; + object-fit: contain; + border-radius: 50%; + } + } + } + } +} + +.details { + flex: 1; + border-left: 1px solid #000; + height: 400px; + position: relative; + overflow: hidden; +} + +.details > p { + padding: 20px; + width: 300px; +} + +.ship-info { + height: 100%; + width: 300px; + margin: auto; + overflow: auto; + background-color: #eee; + border-radius: 4px; + padding: 20px; + position: relative; +} + +.ship-info.ship-loading { + opacity: 0.6; +} + +.ship-info h2 { + font-weight: bold; + text-align: center; + margin-top: 0.3em; +} + +.ship-info img { + width: 100%; + height: 100%; + aspect-ratio: 1; + object-fit: contain; +} + +.ship-info .ship-info__img-wrapper { + margin-top: 20px; + width: 100%; + height: 200px; +} + +.ship-info .ship-info__fetch-time { + position: absolute; + top: 6px; + right: 10px; +} + +.app-error { + position: relative; + background-image: url('/img/broken-ship.webp'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + width: 400px; + height: 400px; + p { + position: absolute; + top: 30%; + left: 50%; + transform: translate(-50%, -50%); + background-color: white; + padding: 6px 12px; + border-radius: 1rem; + font-size: 1.5rem; + font-weight: bold; + width: 300px; + text-align: center; + } +} diff --git a/exercises/07.client-hydration/01.solution.bootstrap/server/async-storage.js b/exercises/07.client-hydration/01.solution.bootstrap/server/async-storage.js new file mode 100644 index 0000000..b9a08b1 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/server/async-storage.js @@ -0,0 +1,3 @@ +import { AsyncLocalStorage } from 'node:async_hooks' + +export const shipDataStorage = new AsyncLocalStorage() diff --git a/exercises/07.client-hydration/01.solution.bootstrap/server/register-rsc-loader.js b/exercises/07.client-hydration/01.solution.bootstrap/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/server/register-rsc-loader.js @@ -0,0 +1,3 @@ +import { register } from 'node:module' + +register('./rsc-loader.js', import.meta.url) diff --git a/exercises/07.client-hydration/01.solution.bootstrap/server/rsc-loader.js b/exercises/07.client-hydration/01.solution.bootstrap/server/rsc-loader.js new file mode 100644 index 0000000..836ca6f --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/server/rsc-loader.js @@ -0,0 +1,23 @@ +import { resolve, load as reactLoad } from 'react-server-dom-esm/node-loader' + +export { resolve } + +async function textLoad(url, context, defaultLoad) { + const result = await defaultLoad(url, context, defaultLoad) + if (result.format === 'module') { + if (typeof result.source === 'string') { + return result + } + return { + source: Buffer.from(result.source).toString('utf8'), + format: 'module', + } + } + return result +} + +export async function load(url, context, defaultLoad) { + return await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) +} diff --git a/exercises/07.client-hydration/01.solution.bootstrap/server/rsc.js b/exercises/07.client-hydration/01.solution.bootstrap/server/rsc.js new file mode 100644 index 0000000..db93f58 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/server/rsc.js @@ -0,0 +1,42 @@ +import closeWithGrace from 'close-with-grace' +import compress from 'compression' +import express from 'express' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { Document } from '../src/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3001 + +const app = express() + +app.use(compress()) + +const moduleBasePath = new URL('../src', import.meta.url).href + +app.get('/:shipId?', function (req, res) { + const shipId = req.params.shipId || null + const search = req.query.search || '' + shipDataStorage.run({ shipId, search }, () => { + const root = h(Document) + const payload = { root } + const { pipe } = renderToPipeableStream(payload, moduleBasePath) + pipe(res) + }) +}) + +const server = app.listen(PORT, () => { + console.log(`✅ RSC: http://localhost:${PORT}`) +}) + +closeWithGrace(async ({ signal, err }) => { + if (err) console.error('Shutting down server due to error', err) + else console.log('Shutting down server due to signal', signal) + + await new Promise((resolve, reject) => { + server.close(err => { + if (err) reject(err) + else resolve() + }) + }) +}) diff --git a/exercises/07.client-hydration/01.solution.bootstrap/server/ssr.js b/exercises/07.client-hydration/01.solution.bootstrap/server/ssr.js new file mode 100644 index 0000000..dfba8b3 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/server/ssr.js @@ -0,0 +1,171 @@ +import http from 'node:http' +import { createRequire } from 'node:module' +import path from 'node:path' +import closeWithGrace from 'close-with-grace' +import compress from 'compression' +import express from 'express' +import { createElement as h, use } from 'react' +import { renderToPipeableStream } from 'react-dom/server' +import { createFromNodeStream } from 'react-server-dom-esm/client' +import { RouterContext } from '../src/router.js' + +const moduleBasePath = new URL('../src', import.meta.url).href + +const PORT = process.env.PORT || 3000 +const RSC_PORT = process.env.RSC_PORT || 3001 +const RSC_ORIGIN = new URL(`http://localhost:${RSC_PORT}`) + +const app = express() + +app.use(compress()) + +function request(options, body) { + return new Promise((resolve, reject) => { + const req = http.request(options, res => { + resolve(res) + }) + req.on('error', e => { + reject(e) + }) + body.pipe(req) + }) +} + +app.head('/', async (req, res) => { + res.status(200).end() +}) + +app.use(express.static('public')) +app.use('/js/src', express.static('src')) +app.use('/js/react-server-dom-esm/client', (req, res) => { + const require = createRequire(import.meta.url) + const pkgPath = require.resolve('react-server-dom-esm') + const modulePath = path.join( + path.dirname(pkgPath), + 'esm', + 'react-server-dom-esm-client.browser.development.js', + ) + res.sendFile(modulePath) +}) + +app.all('/:shipId?', async function (req, res) { + // Proxy the request to the rsc server. + const proxiedHeaders = { + 'X-Forwarded-Host': req.hostname, + 'X-Forwarded-For': req.ips, + 'X-Forwarded-Port': PORT, + 'X-Forwarded-Proto': req.protocol, + } + if (req.get('Content-Type')) { + proxiedHeaders['Content-Type'] = req.get('Content-Type') + } + + const promiseForData = request( + { + host: RSC_ORIGIN.hostname, + port: RSC_ORIGIN.port, + method: req.method, + path: req.url, + headers: proxiedHeaders, + }, + req, + ) + + if (req.accepts('text/html')) { + try { + const rscResponse = await promiseForData + const moduleBaseURL = '/js/src' + + // For HTML, we're a "client" emulator that runs the client code, + // so we start by consuming the RSC payload. This needs the local file path + // to load the source files from as well as the URL path for preloads. + + let contentPromise + function Root() { + contentPromise ??= createFromNodeStream( + rscResponse, + moduleBasePath, + moduleBaseURL, + ) + const content = use(contentPromise) + return content.root + } + const location = req.url + const navigate = () => { + throw new Error('navigate cannot be called on the server') + } + const isPending = false + const routerValue = { + location, + nextLocation: location, + navigate, + isPending, + } + const { pipe } = renderToPipeableStream( + h(RouterContext.Provider, { value: routerValue }, h(Root)), + { + bootstrapModules: ['/js/src/index.js'], + importMap: { + imports: { + react: + 'https://esm.sh/react@0.0.0-experimental-2b036d3f1-20240327?pin=v126&dev', + 'react-dom': + 'https://esm.sh/react-dom@0.0.0-experimental-2b036d3f1-20240327?pin=v126&dev', + 'react-dom/': + 'https://esm.sh/react-dom@0.0.0-experimental-2b036d3f1-20240327&pin=v126&dev/', + 'react-error-boundary': + 'https://esm.sh/react-error-boundary@4.0.13?pin=126&dev', + 'react-server-dom-esm/client': '/js/react-server-dom-esm/client', + }, + }, + }, + ) + pipe(res) + } catch (e) { + console.error(`Failed to SSR: ${e.stack}`) + res.statusCode = 500 + res.end(`Failed to SSR: ${e.stack}`) + } + } else { + try { + const rscResponse = await promiseForData + + // Forward all headers from the RSC response to the client response + Object.entries(rscResponse.headers).forEach(([header, value]) => { + res.set(header, value) + }) + + if (req.get('rsc-action')) { + res.set('Content-type', 'text/x-component') + } + + rscResponse.on('data', data => { + res.write(data) + res.flush() + }) + rscResponse.on('end', () => { + res.end() + }) + } catch (e) { + console.error(`Failed to proxy request: ${e.stack}`) + res.statusCode = 500 + res.end(`Failed to proxy request: ${e.stack}`) + } + } +}) + +const server = app.listen(PORT, () => { + console.log(`✅ SSR: http://localhost:${PORT}`) +}) + +closeWithGrace(async ({ signal, err }) => { + if (err) console.error('Shutting down server due to error', err) + else console.log('Shutting down server due to signal', signal) + + await new Promise((resolve, reject) => { + server.close(err => { + if (err) reject(err) + else resolve() + }) + }) +}) diff --git a/exercises/07.client-hydration/01.solution.bootstrap/src/app.js b/exercises/07.client-hydration/01.solution.bootstrap/src/app.js new file mode 100644 index 0000000..fd4e265 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/src/app.js @@ -0,0 +1,79 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { ShipDetailsPendingTransition } from './ship-details-pending.js' +import { ShipDetails, ShipFallback, ShipError } from './ship-details.js' +import { SearchResults, SearchResultsFallback } from './ship-search-results.js' +import { ShipSearch } from './ship-search.js' + +export async function Document() { + return h( + 'html', + { lang: 'en' }, + h( + 'head', + null, + h('meta', { charSet: 'utf-8' }), + h('meta', { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }), + h('title', null, 'Super Simple RSC'), + h('link', { rel: 'stylesheet', href: '/style.css' }), + h('link', { + rel: 'shortcut icon', + type: 'image/svg+xml', + href: '/favicon.svg', + }), + ), + h('body', null, h('div', { className: 'app-wrapper' }, h(App))), + ) +} + +export function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + h( + ErrorBoundary, + { + fallback: h( + 'div', + { className: 'app-error' }, + h('p', null, 'Something went wrong!'), + ), + }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h( + 'div', + { className: 'search' }, + h(ShipSearch, { + search, + results: h(SearchResults, { search }), + fallback: h(SearchResultsFallback), + }), + ), + h( + ShipDetailsPendingTransition, + null, + h( + ErrorBoundary, + { fallback: h(ShipError) }, + shipId + ? h(Suspense, { fallback: h(ShipFallback) }, h(ShipDetails)) + : h('p', null, 'Select a ship from the list to see details'), + ), + ), + ), + ), + ) +} diff --git a/exercises/07.client-hydration/01.solution.bootstrap/src/error-boundary.js b/exercises/07.client-hydration/01.solution.bootstrap/src/error-boundary.js new file mode 100644 index 0000000..9a93e62 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/src/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export { ErrorBoundary } from 'react-error-boundary' diff --git a/exercises/07.client-hydration/01.solution.bootstrap/src/img-utils.js b/exercises/07.client-hydration/01.solution.bootstrap/src/img-utils.js new file mode 100644 index 0000000..a6b082c --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/src/img-utils.js @@ -0,0 +1,24 @@ +const imgCache = new Map() + +export function imgSrc(src) { + const imgPromise = imgCache.get(src) ?? preloadImage(src) + imgCache.set(src, imgPromise) + return imgPromise +} + +function preloadImage(src) { + if (typeof document === 'undefined') return Promise.resolve(src) + + return new Promise(async (resolve, reject) => { + const img = new Image() + img.src = src + img.onload = () => resolve(src) + img.onerror = reject + }) +} + +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/07.client-hydration/01.solution.bootstrap/src/img.js b/exercises/07.client-hydration/01.solution.bootstrap/src/img.js new file mode 100644 index 0000000..a59c5fb --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/src/img.js @@ -0,0 +1,24 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { imgSrc } from './img-utils.js' + +const shipFallbackSrc = '/img/fallback-ship.png' + +export function ShipImg(props) { + return h( + ErrorBoundary, + { fallback: h('img', props), key: props.src }, + h( + Suspense, + { fallback: h('img', { ...props, src: shipFallbackSrc }) }, + h(Img, props), + ), + ) +} + +function Img({ src = '', ...props }) { + src = use(imgSrc(src)) + return h('img', { src, ...props }) +} diff --git a/exercises/07.client-hydration/01.solution.bootstrap/src/index.js b/exercises/07.client-hydration/01.solution.bootstrap/src/index.js new file mode 100644 index 0000000..985e105 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/src/index.js @@ -0,0 +1,101 @@ +'use client' + +import { + createElement as h, + startTransition, + use, + useEffect, + useRef, + useState, + useTransition, +} from 'react' +import ReactDOM from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { RouterContext } from './router.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +function fetchContent(location) { + return fetch(location, { headers: { Accept: 'text/x-component' } }) +} + +const moduleBaseURL = '/js/src' + +const initialLocation = getGlobalLocation() +const initialContentPromise = RSC.createFromFetch( + fetchContent(initialLocation), + { moduleBaseURL }, +) + +export function Root() { + const latestNav = useRef(null) + const [location, setLocation] = useState(getGlobalLocation) + const [nextLocation, setNextLocation] = useState(location) + const [contentPromise, setContentPromise] = useState(initialContentPromise) + const [isPending, startTransition] = useTransition() + + useEffect(() => { + // once the transition has completed, we can update the current location + if (!isPending) setLocation(nextLocation) + }, [isPending]) + + useEffect(() => { + function handlePopState() { + navigate(getGlobalLocation(), { updateHistory: false }) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, []) + + function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { moduleBaseURL }) + } + + async function navigate( + nextLocation, + { updateHistory = true, replace = false } = {}, + ) { + if (updateHistory) { + setNextLocation(nextLocation) + } + const thisNav = Symbol() + latestNav.current = thisNav + + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then(response => { + if (thisNav !== latestNav.current) return + const newLocation = response.headers.get('x-location') + if (updateHistory) { + if (replace) { + window.history.replaceState(null, '', newLocation) + } else { + window.history.pushState(null, '', newLocation) + } + } + return response + }), + ) + + startTransition(() => { + setContentPromise(nextContentPromise) + }) + } + + return h( + RouterContext.Provider, + { + value: { + location, + nextLocation: isPending ? nextLocation : location, + navigate, + isPending, + }, + }, + use(contentPromise).root, + ) +} + +startTransition(() => { + ReactDOM.hydrateRoot(document, h(Root)) +}) diff --git a/exercises/07.client-hydration/01.solution.bootstrap/src/router.js b/exercises/07.client-hydration/01.solution.bootstrap/src/router.js new file mode 100644 index 0000000..248e678 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/src/router.js @@ -0,0 +1,34 @@ +import { createContext, use } from 'react' + +export const RouterContext = createContext() + +export function useRouter() { + const context = use(RouterContext) + if (!context) { + throw new Error('useRouter must be used within a Router') + } + return context +} + +export function parseLocationState(location) { + const url = new URL(location, 'http://example.com') + return { + shipId: url.pathname.split('/').at(1), + search: url.searchParams.get('search'), + } +} + +export function serializeLocationState({ shipId, search }) { + const pathname = shipId ? `/${shipId}` : '/' + const searchParams = new URLSearchParams() + if (search) { + searchParams.set('search', search) + } + return [pathname, searchParams.toString()].filter(Boolean).join('?') +} + +export function mergeLocationState(location, updates) { + const currentState = parseLocationState(location) + const nextState = { ...currentState, ...updates } + return serializeLocationState(nextState) +} diff --git a/exercises/07.client-hydration/01.solution.bootstrap/src/ship-details-pending.js b/exercises/07.client-hydration/01.solution.bootstrap/src/ship-details-pending.js new file mode 100644 index 0000000..0d98be6 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/src/ship-details-pending.js @@ -0,0 +1,21 @@ +'use client' + +import { createElement as h } from 'react' +import { parseLocationState, useRouter } from './router.js' +import { useSpinDelay } from './spin-delay.js' + +export function ShipDetailsPendingTransition({ children }) { + const { location, nextLocation } = useRouter() + const previousShipId = parseLocationState(nextLocation).shipId + const nextShipId = parseLocationState(location).shipId + const isShipDetailsPending = useSpinDelay(previousShipId !== nextShipId, { + delay: 300, + minDuration: 350, + }) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/07.client-hydration/01.solution.bootstrap/src/ship-details.js b/exercises/07.client-hydration/01.solution.bootstrap/src/ship-details.js new file mode 100644 index 0000000..1e12822 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/src/ship-details.js @@ -0,0 +1,100 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { getImageUrlForShip } from './img-utils.js' +import { ShipImg } from './img.js' + +export async function ShipDetails() { + const { shipId } = shipDataStorage.getStore() + const ship = await getShip({ shipId }) + const shipImgSrc = getImageUrlForShip(ship.id, { size: 200 }) + return h( + 'div', + { className: 'ship-info' }, + h( + 'div', + { className: 'ship-info__img-wrapper' }, + h(ShipImg, { src: shipImgSrc, alt: ship.name }), + ), + h('section', null, h('h2', null, ship.name)), + h('div', null, 'Top Speed: ', ship.topSpeed, ' ', h('small', null, 'lyh')), + h( + 'section', + null, + ship.weapons.length + ? h( + 'ul', + null, + ship.weapons.map(weapon => + h( + 'li', + { key: weapon.name }, + h('label', null, weapon.name), + ':', + ' ', + h( + 'span', + null, + weapon.damage, + ' ', + h('small', null, '(', weapon.type, ')'), + ), + ), + ), + ) + : h('p', null, 'NOTE: This ship is not equipped with any weapons.'), + ), + ) +} + +export function ShipFallback() { + const { shipId } = shipDataStorage.getStore() + return h( + 'div', + { className: 'ship-info' }, + h( + 'div', + { className: 'ship-info__img-wrapper' }, + h(ShipImg, { + src: getImageUrlForShip(shipId, { size: 200 }), + // TODO: handle this better + alt: shipId, + }), + ), + h('section', null, h('h2', null, 'Loading...')), + h('div', null, 'Top Speed: XX', ' ', h('small', null, 'lyh')), + h( + 'section', + null, + h( + 'ul', + null, + Array.from({ length: 3 }).map((_, i) => + h( + 'li', + { key: i }, + h('label', null, 'loading'), + ':', + ' ', + h('span', null, 'XX ', h('small', null, '(loading)')), + ), + ), + ), + ), + ) +} + +export function ShipError() { + const { shipId } = shipDataStorage.getStore() + return h( + 'div', + { className: 'ship-info' }, + h( + 'div', + { className: 'ship-info__img-wrapper' }, + h('img', { src: '/img/broken-ship.webp', alt: 'broken ship' }), + ), + h('section', null, h('h2', null, 'There was an error')), + h('section', null, 'There was an error loading "', shipId, '"'), + ) +} diff --git a/exercises/07.client-hydration/01.solution.bootstrap/src/ship-search-results.js b/exercises/07.client-hydration/01.solution.bootstrap/src/ship-search-results.js new file mode 100644 index 0000000..a79137c --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/src/ship-search-results.js @@ -0,0 +1,43 @@ +import { createElement as h } from 'react' +import { searchShips } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { getImageUrlForShip, shipFallbackSrc } from './img-utils.js' +import { ShipImg } from './img.js' +import { SelectShipButton } from './ship-search.js' + +export async function SearchResults() { + const { shipId: currentShipId, search } = shipDataStorage.getStore() + const shipResults = await searchShips({ search }) + return shipResults.ships.map(ship => + h( + 'li', + { key: ship.name }, + h( + SelectShipButton, + { shipId: ship.id, highlight: ship.id === currentShipId }, + h(ShipImg, { + src: getImageUrlForShip(ship.id, { size: 20 }), + alt: ship.name, + }), + ship.name, + ), + ), + ) +} + +export function SearchResultsFallback() { + return Array.from({ + length: 12, + }).map((_, i) => + h( + 'li', + { key: i }, + h( + 'a', + { href: '#' }, + h('img', { src: shipFallbackSrc, alt: 'loading' }), + '... loading', + ), + ), + ) +} diff --git a/exercises/07.client-hydration/01.solution.bootstrap/src/ship-search.js b/exercises/07.client-hydration/01.solution.bootstrap/src/ship-search.js new file mode 100644 index 0000000..24a1f03 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/src/ship-search.js @@ -0,0 +1,68 @@ +'use client' + +import { Fragment, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { parseLocationState, mergeLocationState, useRouter } from './router.js' +import { useSpinDelay } from './spin-delay.js' + +export function ShipSearch({ search, results, fallback }) { + const { navigate, location, nextLocation } = useRouter() + const previousSearch = parseLocationState(nextLocation).search + const nextSearch = parseLocationState(location).search + const isShipSearchPending = useSpinDelay(previousSearch !== nextSearch, { + delay: 300, + minDuration: 350, + }) + + return h( + Fragment, + null, + h( + 'form', + { onSubmit: e => e.preventDefault() }, + h('input', { + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + name: 'search', + autoFocus: true, + onChange: event => { + const newLocation = mergeLocationState(location, { + search: event.currentTarget.value, + }) + navigate(newLocation, { replace: true }) + }, + }), + ), + h( + ErrorBoundary, + { + fallback: h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ), + }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipButton({ shipId, highlight, children }) { + const { location, navigate } = useRouter() + return h('a', { + children, + href: `/${shipId}`, + style: { fontWeight: highlight ? 'bold' : 'normal' }, + onClick: event => { + if (event.metaKey || event.ctrlKey) return + event.preventDefault() + const newLocation = mergeLocationState(location, { shipId }) + navigate(newLocation) + }, + }) +} diff --git a/exercises/07.client-hydration/01.solution.bootstrap/src/spin-delay.js b/exercises/07.client-hydration/01.solution.bootstrap/src/spin-delay.js new file mode 100644 index 0000000..e5fd221 --- /dev/null +++ b/exercises/07.client-hydration/01.solution.bootstrap/src/spin-delay.js @@ -0,0 +1,35 @@ +import { useState, useEffect, useRef } from 'react' + +export const defaultOptions = { + delay: 500, + minDuration: 200, +} + +export function useSpinDelay(loading, options) { + options = Object.assign({}, defaultOptions, options) + const [state, setState] = useState('IDLE') + const timeout = useRef(null) + useEffect(() => { + if (loading && state === 'IDLE') { + clearTimeout(timeout.current) + timeout.current = setTimeout(() => { + if (!loading) { + return setState('IDLE') + } + timeout.current = setTimeout(() => { + setState('EXPIRE') + }, options.minDuration) + setState('DISPLAY') + }, options.delay) + setState('DELAY') + } + if (!loading && state !== 'DISPLAY') { + clearTimeout(timeout.current) + setState('IDLE') + } + }, [loading, state, options.delay, options.minDuration]) + useEffect(() => { + return () => clearTimeout(timeout.current) + }, []) + return state === 'DISPLAY' || state === 'EXPIRE' +}