diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index d56dfaa..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @type {import('@types/eslint').Linter.Config} - */ -module.exports = { - env: { - node: true, - es2023: true, - browser: true, - }, - plugins: ['eslint-plugin-import'], - parserOptions: { - sourceType: 'module', - ecmaVersion: 2023, - }, - settings: { - 'import/resolver': { - node: true, - }, - }, - rules: { - 'no-undef': 'error', - 'no-duplicate-case': 'error', - 'no-template-curly-in-string': 'warn', - 'no-undef-init': 'error', - 'no-unneeded-ternary': 'error', - 'no-unreachable': 'error', - 'no-unreachable-loop': 'error', - 'no-unsafe-finally': 'error', - 'no-unsafe-negation': 'error', - 'no-unsafe-optional-chaining': 'error', - 'no-unused-expressions': 'error', - 'no-unused-vars': [ - 'warn', - { - args: 'after-used', - argsIgnorePattern: '^_', - ignoreRestSiblings: true, - varsIgnorePattern: '^ignored', - }, - ], - 'no-use-before-define': ['error', 'nofunc'], - 'no-useless-escape': 'error', - 'no-warning-comments': [ - 'error', - { location: 'anywhere', terms: ['fixme'] }, - ], - strict: 'error', - 'valid-typeof': 'error', - 'import/named': 'error', - 'import/no-unresolved': [ - 'error', - // workaround for - // https://github.com/import-js/eslint-plugin-import/issues/1810 - { ignore: ['@kentcdodds/*', 'react-server-dom-esm/*'] }, - ], - - 'import/no-duplicates': ['warn', { 'prefer-inline': true }], - 'import/consistent-type-specifier-style': ['warn', 'prefer-inline'], - 'import/order': [ - 'warn', - { - alphabetize: { order: 'asc', caseInsensitive: true }, - groups: [ - 'builtin', - 'external', - 'internal', - 'parent', - 'sibling', - 'index', - ], - }, - ], - }, -} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index b4bcb98..6786c2f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -29,6 +29,10 @@ jobs: - name: โ–ถ๏ธ Run setup script run: npm run setup + # TOOD: bring this back when CI is more reliable + # - name: โ–ถ๏ธ Run tests + # run: npm run test ..s + deploy: name: ๐Ÿš€ Deploy runs-on: ubuntu-latest @@ -44,6 +48,6 @@ jobs: - name: ๐Ÿš€ Deploy Production run: flyctl deploy --remote-only - working-directory: kcdshop/deployed + working-directory: ./epicshop env: FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} diff --git a/.gitignore b/.gitignore index b338bcf..43c6741 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ workspace/ **/build/ **/public/build **/playwright-report +**/test-results data.db /playground **/tsconfig.tsbuildinfo diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index fe93d37..0000000 --- a/.prettierignore +++ /dev/null @@ -1,13 +0,0 @@ -node_modules - -**/build/** -**/dist/** -**/.cache/** -**/public/build/** -.env - -**/package.json -**/tsconfig.json - -**/package-lock.json -**/playwright-report/** diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/.vscode/extensions.json b/.vscode/extensions.json index fcfa18b..911c295 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,10 +1,10 @@ { - "recommendations": [ - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode", - "bradlc.vscode-tailwindcss", - "neotan.vscode-auto-restart-typescript-eslint-servers", - "prisma.prisma", - "qwtel.sqlite-viewer" - ] + "recommendations": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "bradlc.vscode-tailwindcss", + "neotan.vscode-auto-restart-typescript-eslint-servers", + "prisma.prisma", + "qwtel.sqlite-viewer" + ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 072aee2..9bfcb13 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,12 @@ { - "typescript.preferences.autoImportFileExcludePatterns": [ - "@remix-run/server-runtime", - "@remix-run/router", - "react-router-dom", - "react-router" - ], - "workbench.editorAssociations": { - "*.db": "sqlite-viewer.view" - } + "typescript.preferences.autoImportFileExcludePatterns": [ + "@remix-run/server-runtime", + "@remix-run/router", + "react-router-dom", + "react-router" + ], + "javascript.preferences.importModuleSpecifierEnding": "js", + "workbench.editorAssociations": { + "*.db": "sqlite-viewer.view" + } } diff --git a/README.md b/README.md index e3605f6..a40912d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@
-

React Server Components

+

React Server Components and Functions

- Workshop tagline + Understand React Server Components and Server Functions by building a framework with them.

- Workshop summary + In this workshop we'll be building a framework built on React Server Components and Server Functions from scratch. No build tools. No TypeScript, no Vite, no JSX. Just the Browser, Node.js, and React. This is how you develop a deep understanding of something. Let's go!

@@ -13,7 +13,7 @@
[npm]: https://www.npmjs.com/ [node]: https://nodejs.org diff --git a/epicshop/.diffignore b/epicshop/.diffignore new file mode 100644 index 0000000..f8147ae --- /dev/null +++ b/epicshop/.diffignore @@ -0,0 +1,2 @@ +*.webp +tests/ diff --git a/epicshop/.npmrc b/epicshop/.npmrc new file mode 100644 index 0000000..668efa1 --- /dev/null +++ b/epicshop/.npmrc @@ -0,0 +1,2 @@ +legacy-peer-deps=true +registry=https://registry.npmjs.org/ diff --git a/epicshop/Dockerfile b/epicshop/Dockerfile new file mode 100644 index 0000000..5193139 --- /dev/null +++ b/epicshop/Dockerfile @@ -0,0 +1,27 @@ +FROM node:24-bookworm-slim as base + +RUN apt-get update && apt-get install -y git + +ENV EPICSHOP_GITHUB_REPO=https://github.com/epicweb-dev/react-server-components +ENV EPICSHOP_CONTEXT_CWD="/myapp/workshop-content" +ENV EPICSHOP_HOME_DIR="/myapp/.epicshop" +ENV EPICSHOP_DEPLOYED="true" +ENV EPICSHOP_DISABLE_WATCHER="true" +ENV FLY="true" +ENV PORT="8080" +ENV NODE_ENV="production" + +WORKDIR /myapp + +# Clone the workshop repo during build time, excluding database files +RUN git clone --depth 1 ${EPICSHOP_GITHUB_REPO} ${EPICSHOP_CONTEXT_CWD} + +ADD . . + +RUN npm install --omit=dev + +RUN cd ${EPICSHOP_CONTEXT_CWD} && \ + npx epicshop warm + +CMD cd ${EPICSHOP_CONTEXT_CWD} && \ + npx epicshop start diff --git a/kcdshop/fix-watch.js b/epicshop/fix-watch.js similarity index 93% rename from kcdshop/fix-watch.js rename to epicshop/fix-watch.js index 9b51280..9deae76 100644 --- a/kcdshop/fix-watch.js +++ b/epicshop/fix-watch.js @@ -27,7 +27,7 @@ watcher // Only act if path contains two slashes (excluding the leading `./`) debouncedRun() }) - .on('error', error => console.log(`Watcher error: ${error}`)) + .on('error', (error) => console.log(`Watcher error: ${error}`)) /** * Simple debounce implementation @@ -54,7 +54,7 @@ async function run() { await $({ stdio: 'inherit', cwd: workshopRoot, - })`node ./kcdshop/fix.js` + })`node ./epicshop/fix.js` } catch (error) { throw error } finally { diff --git a/epicshop/fix.js b/epicshop/fix.js new file mode 100644 index 0000000..2770eb3 --- /dev/null +++ b/epicshop/fix.js @@ -0,0 +1,111 @@ +// This should run by node without any dependencies +// because you may need to run it without deps. + +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const here = (...p) => path.join(__dirname, ...p) +// const VERBOSE = false +// const logVerbose = (...args) => (VERBOSE ? console.log(...args) : undefined) + +const workshopRoot = here('..') +const examples = (await readDir(here('../examples'))).map((dir) => + here(`../examples/${dir}`), +) +const exercises = (await readDir(here('../exercises'))) + .map((name) => here(`../exercises/${name}`)) + .filter((filepath) => fs.statSync(filepath).isDirectory()) +const exerciseApps = ( + await Promise.all( + exercises.flatMap(async (exercise) => { + return (await readDir(exercise)) + .filter((dir) => { + return /(problem|solution)/.test(dir) + }) + .map((dir) => path.join(exercise, dir)) + }), + ) +).flat() +const exampleApps = (await readDir(here('../examples'))).map((dir) => + here(`../examples/${dir}`), +) +const apps = [...exampleApps, ...exerciseApps] + +const appsWithPkgJson = [...examples, ...apps].filter((app) => { + const pkgjsonPath = path.join(app, 'package.json') + return exists(pkgjsonPath) +}) + +// update the package.json file name property +// to match the parent directory name + directory name +// e.g. exercises/01-goo/problem.01-great +// name: "exercises__sep__01-goo.problem__sep__01-great" + +function relativeToWorkshopRoot(dir) { + return dir.replace(`${workshopRoot}${path.sep}`, '') +} + +await updatePkgNames() +await copyTestFiles() + +async function updatePkgNames() { + for (const file of appsWithPkgJson) { + const pkgjsonPath = path.join(file, 'package.json') + const pkg = JSON.parse(await fs.promises.readFile(pkgjsonPath, 'utf8')) + pkg.name = relativeToWorkshopRoot(file).replace(/\\|\//g, '__sep__') + const written = await writeIfNeeded( + pkgjsonPath, + `${JSON.stringify(pkg, null, 2)}\n`, + ) + if (written) { + console.log(`updated ${path.relative(process.cwd(), pkgjsonPath)}`) + } + } +} + +async function copyTestFiles() { + for (const app of exerciseApps) { + if (app.includes('problem')) { + const solutionApp = app.replace('problem', 'solution') + const testDir = path.join(solutionApp, 'tests') + const destTestDir = path.join(app, 'tests') + + if (exists(testDir)) { + // Remove existing test directory in problem app if it exists + if (exists(destTestDir)) { + await fs.promises.rm(destTestDir, { recursive: true, force: true }) + } + + // Copy the entire test directory from solution to problem + await fs.promises.cp(testDir, destTestDir, { recursive: true }) + } + } + } +} + +async function writeIfNeeded(filepath, content) { + const oldContent = await fs.promises.readFile(filepath, 'utf8') + if (oldContent !== content) { + await fs.promises.writeFile(filepath, content) + } + return oldContent !== content +} + +function exists(p) { + if (!p) return false + try { + fs.statSync(p) + return true + } catch { + return false + } +} + +async function readDir(dir) { + if (exists(dir)) { + return fs.promises.readdir(dir) + } + return [] +} diff --git a/epicshop/fly.yaml b/epicshop/fly.yaml new file mode 100644 index 0000000..ca316e8 --- /dev/null +++ b/epicshop/fly.yaml @@ -0,0 +1,51 @@ +# See https://fly.io/docs/reference/configuration/ for information about how to use this file. +# + +app: 'epicweb-dev-react-server-components' +primary_region: sjc +kill_signal: SIGINT +kill_timeout: 5s +swap_size_mb: 512 + +experimental: + auto_rollback: true + + attached: + secrets: {} + +services: + - processes: + - app + protocol: tcp + internal_port: 8080 + + ports: + - port: 80 + + handlers: + - http + force_https: true + - port: 443 + + handlers: + - tls + - http + + concurrency: + type: connections + hard_limit: 100 + soft_limit: 80 + + tcp_checks: + - interval: 15s + timeout: 2s + grace_period: 1s + + http_checks: + - interval: 10s + timeout: 2s + grace_period: 5s + method: get + path: /resources/healthcheck + protocol: http + tls_skip_verify: false diff --git a/epicshop/package-lock.json b/epicshop/package-lock.json new file mode 100644 index 0000000..fb4d0ee --- /dev/null +++ b/epicshop/package-lock.json @@ -0,0 +1,13657 @@ +{ + "name": "epicshop", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@epic-web/workshop-app": "^6.19.5", + "@epic-web/workshop-cli": "^6.19.5", + "@epic-web/workshop-utils": "^6.19.5", + "enquirer": "^2.4.1", + "execa": "^9.5.2", + "match-sorter": "^8.0.0", + "p-limit": "^6.2.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", + "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", + "license": "MIT" + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/utils": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", + "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", + "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.10", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.10", + "@babel/parser": "^7.26.10", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.10", + "@babel/types": "^7.26.10", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/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==", + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", + "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", + "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.26.8", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", + "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", + "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", + "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.27.0", + "@babel/types": "^7.27.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", + "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.27.0", + "@babel/parser": "^7.27.0", + "@babel/template": "^7.27.0", + "@babel/types": "^7.27.0", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/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==", + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", + "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", + "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", + "license": "MIT" + }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", + "license": "ISC", + "dependencies": { + "cookie": "^0.7.2" + } + }, + "node_modules/@bundled-es-modules/cookie/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "license": "ISC", + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "license": "ISC", + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "license": "Apache-2.0" + }, + "node_modules/@conform-to/dom": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@conform-to/dom/-/dom-1.8.1.tgz", + "integrity": "sha512-Sg4Jz31ZyiqbFHAKOmG+sVHtukku2Xmxv9ObYzj/TrKMXF1R9qN3ShucAIZWtRaw9l9vZGiqpDnSyfevmiBxuQ==", + "license": "MIT" + }, + "node_modules/@conform-to/react": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@conform-to/react/-/react-1.8.1.tgz", + "integrity": "sha512-+bm+j0MOeOxrhrjWeoLmpmt+m1djZmPM/qE+AH5+8RewuC6GyXEXPyWAyES9bw9mZJ/WqInVS6Ljrf4gQ0I0zA==", + "license": "MIT", + "dependencies": { + "@conform-to/dom": "1.8.1" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/@conform-to/zod": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@conform-to/zod/-/zod-1.8.1.tgz", + "integrity": "sha512-OO3CvfA1STL68oTKTMEcXWR/pBwWfJDSRVvBiR34gLlkZBV01Kfvmea2kQarFXLCJSANvjUyG52OZe9GxvPUGw==", + "license": "MIT", + "dependencies": { + "@conform-to/dom": "1.8.1" + }, + "peerDependencies": { + "zod": "^3.21.0" + } + }, + "node_modules/@epic-web/cachified": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@epic-web/cachified/-/cachified-5.6.0.tgz", + "integrity": "sha512-mr/MmnDm3dUaCe0lHWwAour1s8E8Pn7i03W9vdA4g7AwB2l2bMFfVc6Ofb7kczL62O8UY289u2NOuv9Ac+ksyg==", + "license": "MIT" + }, + "node_modules/@epic-web/client-hints": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@epic-web/client-hints/-/client-hints-1.3.5.tgz", + "integrity": "sha512-tFIDxdU5NzN5Ak4gcDOPKkj6aF/qNMC0G+K58CTBZIx7CMSjCrxqhuiEbZBKGDAGJcsQLF5uKKlgs6mgqWmB7Q==", + "license": "MIT" + }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "license": "MIT" + }, + "node_modules/@epic-web/remember": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@epic-web/remember/-/remember-1.1.0.tgz", + "integrity": "sha512-FIhO7PFUVEbcnrJOtom8gb4GXog4Z44n4Jxwmw2nkKt4mx8I/q/d0O4tMabjYndM1QX2oXvRYzpZxtP61s2P5A==", + "license": "MIT" + }, + "node_modules/@epic-web/restore-scroll": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/restore-scroll/-/restore-scroll-2.0.0.tgz", + "integrity": "sha512-QgjAXcDGcqsd+BGgM8u2tGRpP4kyrON53Bp8VKguOM/U4uH2N7KMnRq8uMmUMpOPxl8dvzwzz/yOJ9l6HRFVvA==", + "license": "MIT", + "peerDependencies": { + "react": ">=18.0.0", + "react-router": ">=7.0.0" + } + }, + "node_modules/@epic-web/workshop-app": { + "version": "6.19.5", + "resolved": "https://registry.npmjs.org/@epic-web/workshop-app/-/workshop-app-6.19.5.tgz", + "integrity": "sha512-82Dnu5l3I7Ufipwy9ubhex9bJfw2EJh7wrvH72tkXFIDQQYl39mB6PxotvrLlNoRmeJ94oIv3wcn6RCMMRAyrA==", + "dependencies": { + "@conform-to/react": "^1.8.0", + "@conform-to/zod": "^1.8.0", + "@epic-web/cachified": "^5.6.0", + "@epic-web/client-hints": "^1.3.5", + "@epic-web/invariant": "^1.0.0", + "@epic-web/remember": "^1.1.0", + "@epic-web/restore-scroll": "^2.0.0", + "@epic-web/workshop-presence": "6.19.5", + "@epic-web/workshop-utils": "6.19.5", + "@mdx-js/mdx": "^3.1.0", + "@mux/mux-player-react": "^3.5.0", + "@nasa-gcn/remix-seo": "^2.0.1", + "@paralleldrive/cuid2": "^2.2.2", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toast": "^1.2.14", + "@radix-ui/react-tooltip": "^1.2.7", + "@react-router/express": "^7.6.3", + "@react-router/node": "^7.6.3", + "@react-router/remix-routes-option-adapter": "^7.6.3", + "@resvg/resvg-js": "^2.6.2", + "@sentry/profiling-node": "^9.35.0", + "@sentry/react-router": "^9.35.0", + "@sindresorhus/slugify": "^2.2.1", + "address": "^2.0.3", + "ansi-to-html": "^0.7.2", + "chalk": "^5.4.1", + "chokidar": "^4.0.3", + "close-with-grace": "^2.2.0", + "clsx": "^2.1.1", + "compression": "^1.8.0", + "confetti-react": "^2.6.0", + "cookie": "^1.0.2", + "cross-env": "^7.0.3", + "cross-spawn": "^7.0.6", + "dotenv": "^17.0.1", + "esbuild": "^0.25.5", + "etag": "^1.8.1", + "execa": "^9.6.0", + "express": "^5.1.0", + "fkill": "^9.0.0", + "framer-motion": "^12.23.0", + "fs-extra": "^11.3.0", + "get-port": "^7.1.0", + "glob": "^11.0.3", + "isbot": "^5.1.28", + "lru-cache": "^11.1.0", + "md5-hex": "^5.0.0", + "mdx-bundler": "^10.1.1", + "mermaid": "^11.8.0", + "mime-types": "^3.0.1", + "morgan": "^1.10.0", + "msw": "^2.10.2", + "open": "^10.1.2", + "openid-client": "^6.6.2", + "p-queue": "^8.1.0", + "partysocket": "^1.1.4", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router": "^7.6.3", + "remix-flat-routes": "^0.8.5", + "remix-utils": "^8.7.0", + "satori": "^0.15.2", + "semver": "^7.7.2", + "shell-quote": "^1.8.3", + "sonner": "^2.0.6", + "source-map-support": "^0.5.21", + "spin-delay": "^2.0.1", + "tailwind-merge": "^2.6.0", + "vite-env-only": "^3.0.3", + "ws": "^8.18.3", + "zod": "^3.25.71" + }, + "engines": { + "node": "20 || 22 || 24" + } + }, + "node_modules/@epic-web/workshop-app/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@epic-web/workshop-cli": { + "version": "6.19.5", + "resolved": "https://registry.npmjs.org/@epic-web/workshop-cli/-/workshop-cli-6.19.5.tgz", + "integrity": "sha512-rH3aXOrMB20Myhvo0Ba6AradQhue+jOs6tnRCBbDypxAu9ZyByDryaeroXeJDSIK4+dOA5xxHyUHfyptWuYtCg==", + "dependencies": { + "@epic-web/workshop-utils": "6.19.5", + "chalk": "^5.3.0", + "close-with-grace": "^2.1.0", + "get-port": "^7.1.0", + "open": "^8.4.2", + "yargs": "^17.7.2" + }, + "bin": { + "epicshop": "dist/esm/cli.js" + } + }, + "node_modules/@epic-web/workshop-cli/node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@epic-web/workshop-cli/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@epic-web/workshop-cli/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@epic-web/workshop-cli/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@epic-web/workshop-presence": { + "version": "6.19.5", + "resolved": "https://registry.npmjs.org/@epic-web/workshop-presence/-/workshop-presence-6.19.5.tgz", + "integrity": "sha512-/4T22e/yKzzyXLFRvFdg4MpiW0639l1pIn0ljwOp3gUZpjcYXzklqhw9x03W4YCNZBJfQ/T6NT6GE8PmJNofmg==", + "dependencies": { + "@epic-web/workshop-utils": "6.19.5", + "zod": "^3.25.71" + } + }, + "node_modules/@epic-web/workshop-utils": { + "version": "6.19.5", + "resolved": "https://registry.npmjs.org/@epic-web/workshop-utils/-/workshop-utils-6.19.5.tgz", + "integrity": "sha512-cu127NnpSvKo2+NCW5393FPl7zGBD5YWqb0aBKWEbFpSXAnrl4T+/rJcYAqyE1qj7eLUxClGj3dKnY2u3y+p5Q==", + "dependencies": { + "@epic-web/cachified": "^5.6.0", + "@epic-web/invariant": "^1.0.0", + "@epic-web/remember": "^1.1.0", + "@kentcdodds/md-temp": "^10.0.1", + "@mdx-js/mdx": "^3.1.0", + "@paralleldrive/cuid2": "^2.2.2", + "@playwright/test": "^1.53.2", + "@react-router/node": "^7.6.3", + "@sentry/react-router": "^9.40.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", + "@total-typescript/ts-reset": "^0.6.1", + "@types/chai": "^5.2.2", + "@types/chai-dom": "^1.11.3", + "@vitest/expect": "^3.2.4", + "chai": "^5.2.0", + "chai-dom": "^1.12.1", + "chalk": "^5.4.1", + "chokidar": "^4.0.3", + "close-with-grace": "^2.2.0", + "cookie": "^1.0.2", + "cross-spawn": "^7.0.6", + "dayjs": "^1.11.13", + "esbuild": "^0.25.5", + "execa": "^9.6.0", + "find-process": "^1.4.10", + "fkill": "^9.0.0", + "fs-extra": "^11.3.0", + "globby": "^14.1.0", + "ignore": "^7.0.5", + "json5": "^2.2.3", + "lru-cache": "^11.1.0", + "lz-string": "^1.5.0", + "md5-hex": "^5.0.0", + "mdast-util-mdx-jsx": "^3.2.0", + "mdx-bundler": "^10.1.1", + "p-queue": "^8.1.0", + "parse-git-diff": "^0.0.19", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router": "^7.7.0", + "rehype": "^13.0.2", + "rehype-autolink-headings": "^7.1.0", + "remark": "^15.0.1", + "remark-emoji": "^5.0.1", + "remark-gfm": "^4.0.1", + "shiki": "^3.7.0", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "zod": "^3.25.71" + } + }, + "node_modules/@esbuild-plugins/node-resolve": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-resolve/-/node-resolve-0.2.2.tgz", + "integrity": "sha512-+t5FdX3ATQlb53UFDBRb4nqjYBz492bIrnVWvpQHpzZlu9BQL5HasMZhqc409ygUwOWCXZhrWr6NyZ6T6Y+cxw==", + "license": "ISC", + "dependencies": { + "@types/resolve": "^1.17.1", + "debug": "^4.3.1", + "escape-string-regexp": "^4.0.0", + "resolve": "^1.19.0" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild-plugins/node-resolve/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@esbuild-plugins/node-resolve/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@esbuild-plugins/node-resolve/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==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fal-works/esbuild-plugin-global-externals": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz", + "integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==", + "license": "MIT" + }, + "node_modules/@floating-ui/core": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", + "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", + "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.2", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", + "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.2" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", + "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.0.0", + "@antfu/utils": "^8.1.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.0", + "globals": "^15.14.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.0.0", + "mlly": "^1.7.4" + } + }, + "node_modules/@iconify/utils/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@iconify/utils/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@iconify/utils/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==", + "license": "MIT" + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.13", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.13.tgz", + "integrity": "sha512-EkCtvp67ICIVVzjsquUiVSd+V5HRGOGQfsqA4E4vMWhYnB7InUL0pa0TIWt1i+OfP16Gkds8CdIu6yGZwOM1Yw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.14", + "@inquirer/type": "^3.0.7" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.14", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.14.tgz", + "integrity": "sha512-Ma+ZpOJPewtIYl6HZHZckeX1STvDnHTCB2GVINNUlSEn2Am6LddWwfPkIGY0IUFVjUUrr/93XlBwTK6mfLjf0A==", + "license": "MIT", + "dependencies": { + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@inquirer/core/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==", + "license": "MIT" + }, + "node_modules/@inquirer/core/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==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", + "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", + "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kentcdodds/md-temp": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@kentcdodds/md-temp/-/md-temp-10.0.1.tgz", + "integrity": "sha512-AN6jXnByJUPY5yckpPD9m9Wvitat2/uheYPznisrh532BOG3A1nkdPnM7LNtji9u2aEvAfFMRN51b8Udw2p0mA==", + "license": "MIT", + "dependencies": { + "escape-goat": "^4.0.0", + "parse-numeric-range": "^1.3.0", + "shiki": "^3.7.0", + "tinypool": "^1.1.1", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0" + } + }, + "node_modules/@mdx-js/esbuild": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mdx-js/esbuild/-/esbuild-3.1.0.tgz", + "integrity": "sha512-Jk42xUb1SEJxh6n2GBAtJjQISFIZccjz8XVEsHVhrlvZJAJziIxR9KyaFF6nTeTB/jCAFQGDgO7+oMRH/ApRsg==", + "license": "MIT", + "dependencies": { + "@mdx-js/mdx": "^3.0.0", + "@types/unist": "^3.0.0", + "source-map": "^0.7.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "esbuild": ">=0.14.0" + } + }, + "node_modules/@mdx-js/mdx": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz", + "integrity": "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdx": "^2.0.0", + "collapse-white-space": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-scope": "^1.0.0", + "estree-walker": "^3.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "markdown-extensions": "^2.0.0", + "recma-build-jsx": "^1.0.0", + "recma-jsx": "^1.0.0", + "recma-stringify": "^1.0.0", + "rehype-recma": "^1.0.0", + "remark-mdx": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "source-map": "^0.7.0", + "unified": "^11.0.0", + "unist-util-position-from-estree": "^2.0.0", + "unist-util-stringify-position": "^4.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.1.tgz", + "integrity": "sha512-lCQNpV8R4lgsGcjX5667UiuDLk2micCtjtxR1YKbBXvN5w2v+FeLYoHrTSSrjwXdMcDYvE4ZBPvKT31dfeSmmA==", + "license": "MIT", + "dependencies": { + "langium": "3.3.1" + } + }, + "node_modules/@mjackson/node-fetch-server": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz", + "integrity": "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==", + "license": "MIT" + }, + "node_modules/@mswjs/interceptors": { + "version": "0.39.2", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.2.tgz", + "integrity": "sha512-RuzCup9Ct91Y7V79xwCb146RaBRHZ7NBbrIUySumd1rpKqHL5OonaqrGIbug5hNwP/fRyxFMA6ISgw4FTtYFYg==", + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mux/mux-data-google-ima": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@mux/mux-data-google-ima/-/mux-data-google-ima-0.2.8.tgz", + "integrity": "sha512-0ZEkHdcZ6bS8QtcjFcoJeZxJTpX7qRIledf4q1trMWPznugvtajCjCM2kieK/pzkZj1JM6liDRFs1PJSfVUs2A==", + "license": "MIT", + "dependencies": { + "mux-embed": "5.9.0" + } + }, + "node_modules/@mux/mux-player": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@mux/mux-player/-/mux-player-3.5.1.tgz", + "integrity": "sha512-PSi3mPb4LrEh4i3xUdodaEvMrbbpKbL2yaewRjsqBr3PFb+hd/Dp1KtyaAnXaBCHl09hDURUSrqYpg1cZvwDiQ==", + "license": "MIT", + "dependencies": { + "@mux/mux-video": "0.26.1", + "@mux/playback-core": "0.30.1", + "media-chrome": "~4.11.1", + "player.style": "^0.1.9" + } + }, + "node_modules/@mux/mux-player-react": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@mux/mux-player-react/-/mux-player-react-3.5.1.tgz", + "integrity": "sha512-tm32fSo9IBA/J8AD99bp64CyBkmv8jtsn4RhSHgNufvfWJUMBFJ7cfXgLsxiG/VdegpfBLRatMC5YiuZjoZ6yg==", + "license": "MIT", + "dependencies": { + "@mux/mux-player": "3.5.1", + "@mux/playback-core": "0.30.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^17.0.0-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0", + "react": "^17.0.2 || ^17.0.0-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0", + "react-dom": "^17.0.2 || ^17.0.2-0 || ^18 || ^18.0.0-0 || ^19 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@mux/mux-video": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/@mux/mux-video/-/mux-video-0.26.1.tgz", + "integrity": "sha512-gkMdBAgNlB4+krANZHkQFzYWjWeNsJz69y1/hnPtmNQnpvW+O7oc71OffcZrbblyibSxWMQ6MQpYmBVjXlp6sA==", + "license": "MIT", + "dependencies": { + "@mux/mux-data-google-ima": "0.2.8", + "@mux/playback-core": "0.30.1", + "castable-video": "~1.1.10", + "custom-media-element": "~1.4.5", + "media-tracks": "~0.3.3" + } + }, + "node_modules/@mux/playback-core": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/@mux/playback-core/-/playback-core-0.30.1.tgz", + "integrity": "sha512-rnO1NE9xHDyzbAkmE6ygJYcD7cyyMt7xXqWTykxlceaoSXLjUqgp42HDio7Lcidto4x/O4FIa7ztjV2aCBCXgQ==", + "license": "MIT", + "dependencies": { + "hls.js": "~1.6.6", + "mux-embed": "^5.8.3" + } + }, + "node_modules/@nasa-gcn/remix-seo": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nasa-gcn/remix-seo/-/remix-seo-2.0.1.tgz", + "integrity": "sha512-g9biDdYfsdFBnOU7lM+7vPGEXSEMRnWmfVLDQ98pT0PnTT/O3pFuA+s3DA0Mj9IwnAq9IcLs2Wee/aL6fvEA+A==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "@remix-run/react": "^1.0.0 || ^2.0.0", + "@remix-run/server-runtime": "^1.0.0 || ^2.0.0" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "license": "MIT" + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz", + "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", + "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", + "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", + "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.57.2", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.2", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-amqplib": { + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.46.1.tgz", + "integrity": "sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-connect": { + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.43.1.tgz", + "integrity": "sha512-ht7YGWQuV5BopMcw5Q2hXn3I8eG8TH0J/kc/GMcW4CuNTgiP6wCu44BOnucJWL3CmFWaRHI//vWyAhaC8BwePw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/connect": "3.4.38" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-dataloader": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.16.1.tgz", + "integrity": "sha512-K/qU4CjnzOpNkkKO4DfCLSQshejRNAJtd4esgigo/50nxCB6XCyi1dhAblUHM9jG5dRm8eu0FB+t87nIo99LYQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-express": { + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.47.1.tgz", + "integrity": "sha512-QNXPTWteDclR2B4pDFpz0TNghgB33UMjUt14B+BZPmtH1MwUFAfLHBaP5If0Z5NZC+jaH8oF2glgYjrmhZWmSw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-fs": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.19.1.tgz", + "integrity": "sha512-6g0FhB3B9UobAR60BGTcXg4IHZ6aaYJzp0Ki5FhnxyAPt8Ns+9SSvgcrnsN2eGmk3RWG5vYycUGOEApycQL24A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-generic-pool": { + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.43.1.tgz", + "integrity": "sha512-M6qGYsp1cURtvVLGDrPPZemMFEbuMmCXgQYTReC/IbimV5sGrLBjB+/hANUpRZjX67nGLdKSVLZuQQAiNz+sww==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-graphql": { + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.47.1.tgz", + "integrity": "sha512-EGQRWMGqwiuVma8ZLAZnExQ7sBvbOx0N/AE/nlafISPs8S+QtXX+Viy6dcQwVWwYHQPAcuY3bFt3xgoAwb4ZNQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-hapi": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.45.2.tgz", + "integrity": "sha512-7Ehow/7Wp3aoyCrZwQpU7a2CnoMq0XhIcioFuKjBb0PLYfBfmTsFTUyatlHu0fRxhwcRsSQRTvEhmZu8CppBpQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http": { + "version": "0.57.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.57.2.tgz", + "integrity": "sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/instrumentation": "0.57.2", + "@opentelemetry/semantic-conventions": "1.28.0", + "forwarded-parse": "2.1.2", + "semver": "^7.5.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/instrumentation-http/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@opentelemetry/instrumentation-ioredis": { + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.47.1.tgz", + "integrity": "sha512-OtFGSN+kgk/aoKgdkKQnBsQFDiG8WdCxu+UrHr0bXScdAmtSzLSraLo7wFIb25RVHfRWvzI5kZomqJYEg/l1iA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/redis-common": "^0.36.2", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.7.1.tgz", + "integrity": "sha512-OtjaKs8H7oysfErajdYr1yuWSjMAectT7Dwr+axIoZqT9lmEOkD/H/3rgAs8h/NIuEi2imSXD+vL4MZtOuJfqQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-knex": { + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.44.1.tgz", + "integrity": "sha512-U4dQxkNhvPexffjEmGwCq68FuftFK15JgUF05y/HlK3M6W/G2iEaACIfXdSnwVNe9Qh0sPfw8LbOPxrWzGWGMQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-koa": { + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.47.1.tgz", + "integrity": "sha512-l/c+Z9F86cOiPJUllUCt09v+kICKvT+Vg1vOAJHtHPsJIzurGayucfCMq2acd/A/yxeNWunl9d9eqZ0G+XiI6A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-lru-memoizer": { + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.44.1.tgz", + "integrity": "sha512-5MPkYCvG2yw7WONEjYj5lr5JFehTobW7wX+ZUFy81oF2lr9IPfZk9qO+FTaM0bGEiymwfLwKe6jE15nHn1nmHg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongodb": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.52.0.tgz", + "integrity": "sha512-1xmAqOtRUQGR7QfJFfGV/M2kC7wmI2WgZdpru8hJl3S0r4hW0n3OQpEHlSGXJAaNFyvT+ilnwkT+g5L4ljHR6g==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mongoose": { + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.46.1.tgz", + "integrity": "sha512-3kINtW1LUTPkiXFRSSBmva1SXzS/72we/jL22N+BnF3DFcoewkdkHPYOIdAAk9gSicJ4d5Ojtt1/HeibEc5OQg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql": { + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.45.1.tgz", + "integrity": "sha512-TKp4hQ8iKQsY7vnp/j0yJJ4ZsP109Ht6l4RHTj0lNEG1TfgTrIH5vJMbgmoYXWzNHAqBH2e7fncN12p3BP8LFg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/mysql": "2.15.26" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-mysql2": { + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.45.2.tgz", + "integrity": "sha512-h6Ad60FjCYdJZ5DTz1Lk2VmQsShiViKe0G7sYikb0GHI0NVvApp2XQNRHNjEMz87roFttGPLHOYVPlfy+yVIhQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/sql-common": "^0.40.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-pg": { + "version": "0.51.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.51.1.tgz", + "integrity": "sha512-QxgjSrxyWZc7Vk+qGSfsejPVFL1AgAJdSBMYZdDUbwg730D09ub3PXScB9d04vIqPriZ+0dqzjmQx0yWKiCi2Q==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.26.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@opentelemetry/sql-common": "^0.40.1", + "@types/pg": "8.6.1", + "@types/pg-pool": "2.0.6" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-redis-4": { + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.46.1.tgz", + "integrity": "sha512-UMqleEoabYMsWoTkqyt9WAzXwZ4BlFZHO40wr3d5ZvtjKCHlD4YXLm+6OLCeIi/HkX7EXvQaz8gtAwkwwSEvcQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/redis-common": "^0.36.2", + "@opentelemetry/semantic-conventions": "^1.27.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-tedious": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.18.1.tgz", + "integrity": "sha512-5Cuy/nj0HBaH+ZJ4leuD7RjgvA844aY2WW+B5uLcWtxGjRZl3MNLuxnNg5DYWZNPO+NafSSnra0q49KWAHsKBg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/tedious": "^4.0.14" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-undici": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.10.1.tgz", + "integrity": "sha512-rkOGikPEyRpMCmNu9AQuV5dtRlDmJp2dK5sw8roVshAGoB6hH/3QjDtRhdwd75SsJwgynWUNRUYe0wAkTo16tQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.8.0", + "@opentelemetry/instrumentation": "^0.57.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.7.0" + } + }, + "node_modules/@opentelemetry/instrumentation/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@opentelemetry/redis-common": { + "version": "0.36.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz", + "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", + "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", + "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "1.30.1", + "@opentelemetry/resources": "1.30.1", + "@opentelemetry/semantic-conventions": "1.28.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", + "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.34.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.34.0.tgz", + "integrity": "sha512-aKcOkyrorBGlajjRdVoJWHTxfxO1vCNHLJVlSDaRHDIdjU+pX8IYQPvPDkYiujKLbRnWU+1TBwEt0QRgSm4SGA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@opentelemetry/sql-common": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz", + "integrity": "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "^1.1.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0" + } + }, + "node_modules/@paralleldrive/cuid2": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", + "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.1.5" + } + }, + "node_modules/@playwright/test": { + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz", + "integrity": "sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.54.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@prisma/instrumentation": { + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.10.1.tgz", + "integrity": "sha512-JC8qzgEDuFKjuBsqrZvXHINUb12psnE6Qy3q5p2MBhalC1KW1MBBUwuonx6iS5TCfCdtNslHft8uc2r+EdLWWg==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.8" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.11.tgz", + "integrity": "sha512-l3W5D54emV2ues7jjeG1xcyN7S3jnK3zE2zHqgn0CmMsy9lNJwmgcrmaxS+7ipw15FAivzKNzH3d5EcGoFKw0A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collapsible": "1.1.11", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.11.tgz", + "integrity": "sha512-2qrRsVGSCYasSz1RFOorXwl0H7g7J1frQtgpQgYrt+MOidtPAINHn9CPovQXb83r8ahapdx3Tu0fa/pdFFSdPg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", + "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", + "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", + "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", + "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz", + "integrity": "sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.14.tgz", + "integrity": "sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz", + "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@react-router/express": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.6.3.tgz", + "integrity": "sha512-45wLv2pNVDfnd4mZXYaxbqGE2wOzisQQAXSCHrWhkUn9CvJkaqC9cx82rzfB1UnGvyeupZxGgLxaG0b38pTEOA==", + "license": "MIT", + "dependencies": { + "@react-router/node": "7.6.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "express": "^4.17.1 || ^5", + "react-router": "7.6.3", + "typescript": "^5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@react-router/node": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.6.3.tgz", + "integrity": "sha512-CgqYAGjrfW/Al0LbWhQ60joDci5/H3ix4IU5UwlKLtqmNPzuSUTBkCrxit3jHuMYqaBaGfyRpT7kIeb1YZ4nqA==", + "license": "MIT", + "dependencies": { + "@mjackson/node-fetch-server": "^0.2.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react-router": "7.6.3", + "typescript": "^5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@react-router/remix-routes-option-adapter": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/@react-router/remix-routes-option-adapter/-/remix-routes-option-adapter-7.6.3.tgz", + "integrity": "sha512-yfX/faADtFP5FglkLOILVrVnOy9+muGAkGSb0V8J7my6FH5ZyA1MHtkWyihgcuJvpF27mVwA5O1OoLOELSl5qA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@react-router/dev": "^7.6.3", + "typescript": "^5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@resvg/resvg-js": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", + "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==", + "license": "MPL-2.0", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@resvg/resvg-js-android-arm-eabi": "2.6.2", + "@resvg/resvg-js-android-arm64": "2.6.2", + "@resvg/resvg-js-darwin-arm64": "2.6.2", + "@resvg/resvg-js-darwin-x64": "2.6.2", + "@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2", + "@resvg/resvg-js-linux-arm64-gnu": "2.6.2", + "@resvg/resvg-js-linux-arm64-musl": "2.6.2", + "@resvg/resvg-js-linux-x64-gnu": "2.6.2", + "@resvg/resvg-js-linux-x64-musl": "2.6.2", + "@resvg/resvg-js-win32-arm64-msvc": "2.6.2", + "@resvg/resvg-js-win32-ia32-msvc": "2.6.2", + "@resvg/resvg-js-win32-x64-msvc": "2.6.2" + } + }, + "node_modules/@resvg/resvg-js-android-arm-eabi": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz", + "integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-android-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz", + "integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-arm64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz", + "integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-darwin-x64": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz", + "integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm-gnueabihf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz", + "integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz", + "integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-arm64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz", + "integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-gnu": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz", + "integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-linux-x64-musl": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz", + "integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-arm64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz", + "integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-ia32-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz", + "integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==", + "cpu": [ + "ia32" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@resvg/resvg-js-win32-x64-msvc": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz", + "integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@sentry-internal/browser-utils": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.40.0.tgz", + "integrity": "sha512-Ajvz6jN+EEMKrOHcUv2+HlhbRUh69uXhhRoBjJw8sc61uqA2vv3QWyBSmTRoHdTnLGboT5bKEhHIkzVXb+YgEw==", + "license": "MIT", + "dependencies": { + "@sentry/core": "9.40.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/browser-utils/node_modules/@sentry/core": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.40.0.tgz", + "integrity": "sha512-cZkuz6BDna6VXSqvlWnrRsaDx4QBKq1PcfQrqhVz8ljs0M7Gcl+Mtj8dCzUxx12fkYM62hQXG72DEGNlAQpH/Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.40.0.tgz", + "integrity": "sha512-39UbLdGWGvSJ7bAzRnkv91cBdd6fLbdkLVVvqE2ZUfegm7+rH1mRPglmEhw4VE4mQfKZM1zWr/xus2+XPqJcYw==", + "license": "MIT", + "dependencies": { + "@sentry/core": "9.40.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/feedback/node_modules/@sentry/core": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.40.0.tgz", + "integrity": "sha512-cZkuz6BDna6VXSqvlWnrRsaDx4QBKq1PcfQrqhVz8ljs0M7Gcl+Mtj8dCzUxx12fkYM62hQXG72DEGNlAQpH/Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/node-cpu-profiler": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/node-cpu-profiler/-/node-cpu-profiler-2.2.0.tgz", + "integrity": "sha512-oLHVYurqZfADPh5hvmQYS5qx8t0UZzT2u6+/68VXsFruQEOnYJTODKgU3BVLmemRs3WE6kCJjPeFdHVYOQGSzQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "node-abi": "^3.73.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.40.0.tgz", + "integrity": "sha512-WrmCvqbLJQC45IFRVN3k0J5pU5NkdX0e9o6XxjcmDiATKk00RHnW4yajnCJ8J1cPR4918yqiJHPX5xpG08BZNA==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "9.40.0", + "@sentry/core": "9.40.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.40.0.tgz", + "integrity": "sha512-GLoJ4R4Uipd7Vb+0LzSJA2qCyN1J6YalQIoDuOJTfYyykHvKltds5D8a/5S3Q6d8PcL/nxTn93fynauGEZt2Ow==", + "license": "MIT", + "dependencies": { + "@sentry-internal/replay": "9.40.0", + "@sentry/core": "9.40.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay-canvas/node_modules/@sentry/core": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.40.0.tgz", + "integrity": "sha512-cZkuz6BDna6VXSqvlWnrRsaDx4QBKq1PcfQrqhVz8ljs0M7Gcl+Mtj8dCzUxx12fkYM62hQXG72DEGNlAQpH/Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry-internal/replay/node_modules/@sentry/core": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.40.0.tgz", + "integrity": "sha512-cZkuz6BDna6VXSqvlWnrRsaDx4QBKq1PcfQrqhVz8ljs0M7Gcl+Mtj8dCzUxx12fkYM62hQXG72DEGNlAQpH/Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/babel-plugin-component-annotate": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.5.0.tgz", + "integrity": "sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@sentry/browser": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.40.0.tgz", + "integrity": "sha512-qz/1Go817vcsbcIwgrz4/T34vi3oQ4UIqikosuaCTI9wjZvK0HyW3QmLvTbAnsE7G7h6+UZsVkpO5R16IQvQhQ==", + "license": "MIT", + "dependencies": { + "@sentry-internal/browser-utils": "9.40.0", + "@sentry-internal/feedback": "9.40.0", + "@sentry-internal/replay": "9.40.0", + "@sentry-internal/replay-canvas": "9.40.0", + "@sentry/core": "9.40.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/browser/node_modules/@sentry/core": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.40.0.tgz", + "integrity": "sha512-cZkuz6BDna6VXSqvlWnrRsaDx4QBKq1PcfQrqhVz8ljs0M7Gcl+Mtj8dCzUxx12fkYM62hQXG72DEGNlAQpH/Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/bundler-plugin-core": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.5.0.tgz", + "integrity": "sha512-zDzPrhJqAAy2VzV4g540qAZH4qxzisstK2+NIJPZUUKztWRWUV2cMHsyUtdctYgloGkLyGpZJBE3RE6dmP/xqQ==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.18.5", + "@sentry/babel-plugin-component-annotate": "3.5.0", + "@sentry/cli": "2.42.2", + "dotenv": "^16.3.1", + "find-up": "^5.0.0", + "glob": "^9.3.2", + "magic-string": "0.30.8", + "unplugin": "1.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.42.2.tgz", + "integrity": "sha512-spb7S/RUumCGyiSTg8DlrCX4bivCNmU/A1hcfkwuciTFGu8l5CDc2I6jJWWZw8/0enDGxuj5XujgXvU5tr4bxg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.7", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.42.2", + "@sentry/cli-linux-arm": "2.42.2", + "@sentry/cli-linux-arm64": "2.42.2", + "@sentry/cli-linux-i686": "2.42.2", + "@sentry/cli-linux-x64": "2.42.2", + "@sentry/cli-win32-i686": "2.42.2", + "@sentry/cli-win32-x64": "2.42.2" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-darwin": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.42.2.tgz", + "integrity": "sha512-GtJSuxER7Vrp1IpxdUyRZzcckzMnb4N5KTW7sbTwUiwqARRo+wxS+gczYrS8tdgtmXs5XYhzhs+t4d52ITHMIg==", + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-linux-arm": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.2.tgz", + "integrity": "sha512-7udCw+YL9lwq+9eL3WLspvnuG+k5Icg92YE7zsteTzWLwgPVzaxeZD2f8hwhsu+wmL+jNqbpCRmktPteh3i2mg==", + "cpu": [ + "arm" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-linux-arm64": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.2.tgz", + "integrity": "sha512-BOxzI7sgEU5Dhq3o4SblFXdE9zScpz6EXc5Zwr1UDZvzgXZGosUtKVc7d1LmkrHP8Q2o18HcDWtF3WvJRb5Zpw==", + "cpu": [ + "arm64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-linux-i686": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.2.tgz", + "integrity": "sha512-Sw/dQp5ZPvKnq3/y7wIJyxTUJYPGoTX/YeMbDs8BzDlu9to2LWV3K3r7hE7W1Lpbaw4tSquUHiQjP5QHCOS7aQ==", + "cpu": [ + "x86", + "ia32" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-linux-x64": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.2.tgz", + "integrity": "sha512-mU4zUspAal6TIwlNLBV5oq6yYqiENnCWSxtSQVzWs0Jyq97wtqGNG9U+QrnwjJZ+ta/hvye9fvL2X25D/RxHQw==", + "cpu": [ + "x64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-win32-i686": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.2.tgz", + "integrity": "sha512-iHvFHPGqgJMNqXJoQpqttfsv2GI3cGodeTq4aoVLU/BT3+hXzbV0x1VpvvEhncJkDgDicJpFLM8sEPHb3b8abw==", + "cpu": [ + "x86", + "ia32" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/@sentry/cli-win32-x64": { + "version": "2.42.2", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.2.tgz", + "integrity": "sha512-vPPGHjYoaGmfrU7xhfFxG7qlTBacroz5NdT+0FmDn6692D8IvpNXl1K+eV3Kag44ipJBBeR8g1HRJyx/F/9ACw==", + "cpu": [ + "x64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/bundler-plugin-core/node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@sentry/cli": { + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.46.0.tgz", + "integrity": "sha512-nqoPl7UCr446QFkylrsRrUXF51x8Z9dGquyf4jaQU+OzbOJMqclnYEvU6iwbwvaw3tu/2DnoZE/Og+Nq1h63sA==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.7", + "progress": "^2.0.3", + "proxy-from-env": "^1.1.0", + "which": "^2.0.2" + }, + "bin": { + "sentry-cli": "bin/sentry-cli" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@sentry/cli-darwin": "2.46.0", + "@sentry/cli-linux-arm": "2.46.0", + "@sentry/cli-linux-arm64": "2.46.0", + "@sentry/cli-linux-i686": "2.46.0", + "@sentry/cli-linux-x64": "2.46.0", + "@sentry/cli-win32-arm64": "2.46.0", + "@sentry/cli-win32-i686": "2.46.0", + "@sentry/cli-win32-x64": "2.46.0" + } + }, + "node_modules/@sentry/cli-darwin": { + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.46.0.tgz", + "integrity": "sha512-5Ll+e5KAdIk9OYiZO8aifMBRNWmNyPjSqdjaHlBC1Qfh7pE3b1zyzoHlsUazG0bv0sNrSGea8e7kF5wIO1hvyg==", + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm": { + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.46.0.tgz", + "integrity": "sha512-WRrLNq/TEX/TNJkGqq6Ad0tGyapd5dwlxtsPbVBrIdryuL1mA7VCBoaHBr3kcwJLsgBHFH0lmkMee2ubNZZdkg==", + "cpu": [ + "arm" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-arm64": { + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.46.0.tgz", + "integrity": "sha512-OEJN8yAjI9y5B4telyqzu27Hi3+S4T8VxZCqJz1+z2Mp0Q/MZ622AahVPpcrVq/5bxrnlZR16+lKh8L1QwNFPg==", + "cpu": [ + "arm64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-i686": { + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.46.0.tgz", + "integrity": "sha512-xko3/BVa4LX8EmRxVOCipV+PwfcK5Xs8lP6lgF+7NeuAHMNL4DqF6iV9rrN8gkGUHCUI9RXSve37uuZnFy55+Q==", + "cpu": [ + "x86", + "ia32" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-linux-x64": { + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.46.0.tgz", + "integrity": "sha512-hJ1g5UEboYcOuRia96LxjJ0jhnmk8EWLDvlGnXLnYHkwy3ree/L7sNgdp/QsY8Z4j2PGO5f22Va+UDhSjhzlfQ==", + "cpu": [ + "x64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux", + "freebsd", + "android" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-arm64": { + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.46.0.tgz", + "integrity": "sha512-mN7cpPoCv2VExFRGHt+IoK11yx4pM4ADZQGEso5BAUZ5duViXB2WrAXCLd8DrwMnP0OE978a7N8OtzsFqjkbNA==", + "cpu": [ + "arm64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-i686": { + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.46.0.tgz", + "integrity": "sha512-6F73AUE3lm71BISUO19OmlnkFD5WVe4/wA1YivtLZTc1RU3eUYJLYxhDfaH3P77+ycDppQ2yCgemLRaA4A8mNQ==", + "cpu": [ + "x86", + "ia32" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/cli-win32-x64": { + "version": "2.46.0", + "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.46.0.tgz", + "integrity": "sha512-yuGVcfepnNL84LGA0GjHzdMIcOzMe0bjPhq/rwPsPN+zu11N+nPR2wV2Bum4U0eQdqYH3iAlMdL5/BEQfuLJww==", + "cpu": [ + "x64" + ], + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@sentry/core": { + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.35.0.tgz", + "integrity": "sha512-bdAtzVQZ/wn4L/m8r2OUCCG/NWr0Q8dyZDwdwvINJaMbyhDRUdQh/MWjrz+id/3JoOL1LigAyTV1h4FJDGuwUQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node": { + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-9.35.0.tgz", + "integrity": "sha512-7ifFqTsa3BtZGRAgqoWqYf7OJizKSyEzQlSixgBc253wyYWiLaVJ15By9Y4ozd+PbgpOPqfDN5B45Y+OxtQnQw==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1", + "@opentelemetry/core": "^1.30.1", + "@opentelemetry/instrumentation": "^0.57.2", + "@opentelemetry/instrumentation-amqplib": "^0.46.1", + "@opentelemetry/instrumentation-connect": "0.43.1", + "@opentelemetry/instrumentation-dataloader": "0.16.1", + "@opentelemetry/instrumentation-express": "0.47.1", + "@opentelemetry/instrumentation-fs": "0.19.1", + "@opentelemetry/instrumentation-generic-pool": "0.43.1", + "@opentelemetry/instrumentation-graphql": "0.47.1", + "@opentelemetry/instrumentation-hapi": "0.45.2", + "@opentelemetry/instrumentation-http": "0.57.2", + "@opentelemetry/instrumentation-ioredis": "0.47.1", + "@opentelemetry/instrumentation-kafkajs": "0.7.1", + "@opentelemetry/instrumentation-knex": "0.44.1", + "@opentelemetry/instrumentation-koa": "0.47.1", + "@opentelemetry/instrumentation-lru-memoizer": "0.44.1", + "@opentelemetry/instrumentation-mongodb": "0.52.0", + "@opentelemetry/instrumentation-mongoose": "0.46.1", + "@opentelemetry/instrumentation-mysql": "0.45.1", + "@opentelemetry/instrumentation-mysql2": "0.45.2", + "@opentelemetry/instrumentation-pg": "0.51.1", + "@opentelemetry/instrumentation-redis-4": "0.46.1", + "@opentelemetry/instrumentation-tedious": "0.18.1", + "@opentelemetry/instrumentation-undici": "0.10.1", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/sdk-trace-base": "^1.30.1", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@prisma/instrumentation": "6.10.1", + "@sentry/core": "9.35.0", + "@sentry/opentelemetry": "9.35.0", + "import-in-the-middle": "^1.14.2", + "minimatch": "^9.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-9.40.0.tgz", + "integrity": "sha512-97JONDa8NxItX0Cz5WQPMd1gQjzodt38qQ0OzZNFvYg2Cpvxob8rxwsNA08Liu7B97rlvsvqMt+Wbgw8SAMfgQ==", + "license": "MIT", + "dependencies": { + "@sentry/core": "9.40.0", + "@sentry/opentelemetry": "9.40.0", + "import-in-the-middle": "^1.14.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", + "@opentelemetry/core": "^1.30.1 || ^2.0.0", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/resources": "^1.30.1 || ^2.0.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + } + }, + "node_modules/@sentry/node-core/node_modules/@sentry/core": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.40.0.tgz", + "integrity": "sha512-cZkuz6BDna6VXSqvlWnrRsaDx4QBKq1PcfQrqhVz8ljs0M7Gcl+Mtj8dCzUxx12fkYM62hQXG72DEGNlAQpH/Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core/node_modules/@sentry/opentelemetry": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.40.0.tgz", + "integrity": "sha512-POQ/ZFmBbi15z3EO9gmTExpxCfW0Ug+WooA8QZPJaizo24gcF5AMOgwuGFwT2YLw/2HdPWjPUPujNNGdCWM6hw==", + "license": "MIT", + "dependencies": { + "@sentry/core": "9.40.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", + "@opentelemetry/core": "^1.30.1 || ^2.0.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + } + }, + "node_modules/@sentry/node/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/opentelemetry": { + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.35.0.tgz", + "integrity": "sha512-XJmSC71KaN+qwYf5EEobLDyWum4FijpIjnpTVTYOrq037uUCpxJEGtgQHq0X+DE/ycVUX/Og2PiAgTeCQEYfDg==", + "license": "MIT", + "dependencies": { + "@sentry/core": "9.35.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", + "@opentelemetry/core": "^1.30.1 || ^2.0.0", + "@opentelemetry/instrumentation": "^0.57.1 || ^0.200.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + } + }, + "node_modules/@sentry/profiling-node": { + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@sentry/profiling-node/-/profiling-node-9.35.0.tgz", + "integrity": "sha512-lUjOMy8/+YbdN4LVDzMyeTNC3zT8JA7EnA2JdttFB6jkLFWASfRHsrX26+O5LP6ajSiOhVbd5CnWHEKy0essiQ==", + "license": "MIT", + "dependencies": { + "@sentry-internal/node-cpu-profiler": "^2.2.0", + "@sentry/core": "9.35.0", + "@sentry/node": "9.35.0" + }, + "bin": { + "sentry-prune-profiler-binaries": "scripts/prune-profiler-binaries.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/react": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.40.0.tgz", + "integrity": "sha512-y00d33qozmQAKroQ4Kk2jxhznprPBOb55SL4LOpNPRHGEomxZCUeM3geltczrf14JsGowCr5+xlT+cZQ2XcNlA==", + "license": "MIT", + "dependencies": { + "@sentry/browser": "9.40.0", + "@sentry/core": "9.40.0", + "hoist-non-react-statics": "^3.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.14.0 || 17.x || 18.x || 19.x" + } + }, + "node_modules/@sentry/react-router": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/react-router/-/react-router-9.40.0.tgz", + "integrity": "sha512-N1RzcV6OuKLFA7klaV0q1e0N0RmQ6rAO+hMjfnopVTsFxgHqpUcI6Ryt+OilQQQcW3ymPq8HZhKBb9lyo1IBRQ==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1", + "@opentelemetry/instrumentation": "0.57.2", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@sentry/browser": "9.40.0", + "@sentry/cli": "^2.46.0", + "@sentry/core": "9.40.0", + "@sentry/node": "9.40.0", + "@sentry/react": "9.40.0", + "@sentry/vite-plugin": "^3.5.0", + "glob": "11.0.1" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@react-router/node": "7.x", + "react": ">=18", + "react-router": "7.x" + } + }, + "node_modules/@sentry/react-router/node_modules/@prisma/instrumentation": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.11.1.tgz", + "integrity": "sha512-mrZOev24EDhnefmnZX7WVVT7v+r9LttPRqf54ONvj6re4XMF7wFTpK2tLJi4XHB7fFp/6xhYbgRel8YV7gQiyA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.8" + } + }, + "node_modules/@sentry/react-router/node_modules/@sentry/core": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.40.0.tgz", + "integrity": "sha512-cZkuz6BDna6VXSqvlWnrRsaDx4QBKq1PcfQrqhVz8ljs0M7Gcl+Mtj8dCzUxx12fkYM62hQXG72DEGNlAQpH/Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/react-router/node_modules/@sentry/node": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-9.40.0.tgz", + "integrity": "sha512-8bVWChXzGH4QmbVw+H/yiJ6zxqPDhnx11fEAP+vpL1UBm1cAV67CoB4eS7OqQdPC8gF/BQb2sqF0TvY/12NPpA==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1", + "@opentelemetry/core": "^1.30.1", + "@opentelemetry/instrumentation": "^0.57.2", + "@opentelemetry/instrumentation-amqplib": "^0.46.1", + "@opentelemetry/instrumentation-connect": "0.43.1", + "@opentelemetry/instrumentation-dataloader": "0.16.1", + "@opentelemetry/instrumentation-express": "0.47.1", + "@opentelemetry/instrumentation-fs": "0.19.1", + "@opentelemetry/instrumentation-generic-pool": "0.43.1", + "@opentelemetry/instrumentation-graphql": "0.47.1", + "@opentelemetry/instrumentation-hapi": "0.45.2", + "@opentelemetry/instrumentation-http": "0.57.2", + "@opentelemetry/instrumentation-ioredis": "0.47.1", + "@opentelemetry/instrumentation-kafkajs": "0.7.1", + "@opentelemetry/instrumentation-knex": "0.44.1", + "@opentelemetry/instrumentation-koa": "0.47.1", + "@opentelemetry/instrumentation-lru-memoizer": "0.44.1", + "@opentelemetry/instrumentation-mongodb": "0.52.0", + "@opentelemetry/instrumentation-mongoose": "0.46.1", + "@opentelemetry/instrumentation-mysql": "0.45.1", + "@opentelemetry/instrumentation-mysql2": "0.45.2", + "@opentelemetry/instrumentation-pg": "0.51.1", + "@opentelemetry/instrumentation-redis-4": "0.46.1", + "@opentelemetry/instrumentation-tedious": "0.18.1", + "@opentelemetry/instrumentation-undici": "0.10.1", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/sdk-trace-base": "^1.30.1", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@prisma/instrumentation": "6.11.1", + "@sentry/core": "9.40.0", + "@sentry/node-core": "9.40.0", + "@sentry/opentelemetry": "9.40.0", + "import-in-the-middle": "^1.14.2", + "minimatch": "^9.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/react-router/node_modules/@sentry/node/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/react-router/node_modules/@sentry/opentelemetry": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.40.0.tgz", + "integrity": "sha512-POQ/ZFmBbi15z3EO9gmTExpxCfW0Ug+WooA8QZPJaizo24gcF5AMOgwuGFwT2YLw/2HdPWjPUPujNNGdCWM6hw==", + "license": "MIT", + "dependencies": { + "@sentry/core": "9.40.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", + "@opentelemetry/core": "^1.30.1 || ^2.0.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + } + }, + "node_modules/@sentry/react-router/node_modules/glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@sentry/react/node_modules/@sentry/core": { + "version": "9.40.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.40.0.tgz", + "integrity": "sha512-cZkuz6BDna6VXSqvlWnrRsaDx4QBKq1PcfQrqhVz8ljs0M7Gcl+Mtj8dCzUxx12fkYM62hQXG72DEGNlAQpH/Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/vite-plugin": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-3.5.0.tgz", + "integrity": "sha512-jUnpTdpicG8wefamw7eNo2uO+Q3KCbOAiF76xH4gfNHSW6TN2hBfOtmLu7J+ive4c0Al3+NEHz19bIPR0lkwWg==", + "license": "MIT", + "dependencies": { + "@sentry/bundler-plugin-core": "3.5.0", + "unplugin": "1.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@shikijs/core": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.8.1.tgz", + "integrity": "sha512-uTSXzUBQ/IgFcUa6gmGShCHr4tMdR3pxUiiWKDm8pd42UKJdYhkAYsAmHX5mTwybQ5VyGDgTjW4qKSsRvGSang==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.8.1", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.8.1.tgz", + "integrity": "sha512-rZRp3BM1llrHkuBPAdYAzjlF7OqlM0rm/7EWASeCcY7cRYZIrOnGIHE9qsLz5TCjGefxBFnwgIECzBs2vmOyKA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.8.1", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.3" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.8.1.tgz", + "integrity": "sha512-KGQJZHlNY7c656qPFEQpIoqOuC4LrxjyNndRdzk5WKB/Ie87+NJCF1xo9KkOUxwxylk7rT6nhlZyTGTC4fCe1g==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.8.1", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.8.1.tgz", + "integrity": "sha512-TjOFg2Wp1w07oKnXjs0AUMb4kJvujML+fJ1C5cmEj45lhjbUXtziT1x2bPQb9Db6kmPhkG5NI2tgYW1/DzhUuQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.8.1" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.8.1.tgz", + "integrity": "sha512-Vu3t3BBLifc0GB0UPg2Pox1naTemrrvyZv2lkiSw3QayVV60me1ujFQwPZGgUTmwXl1yhCPW8Lieesm0CYruLQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.8.1" + } + }, + "node_modules/@shikijs/types": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.8.1.tgz", + "integrity": "sha512-5C39Q8/8r1I26suLh+5TPk1DTrbY/kn3IdWA5HdizR0FhlhD05zx5nKCqhzSfDHH3p4S0ZefxWd77DLV+8FhGg==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/slugify": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", + "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/transliterate": "^1.0.0", + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", + "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "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/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "license": "MIT" + }, + "node_modules/@total-typescript/ts-reset": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@total-typescript/ts-reset/-/ts-reset-0.6.1.tgz", + "integrity": "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==", + "license": "MIT" + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "node_modules/@types/chai-dom": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@types/chai-dom/-/chai-dom-1.11.3.tgz", + "integrity": "sha512-EUEZI7uID4ewzxnU7DJXtyvykhQuwe+etJ1wwOiJyQRTH/ifMWKX+ghiXkxCUvNJ6IQDodf0JXhuP6zZcy2qXQ==", + "license": "MIT", + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/mysql": { + "version": "2.15.26", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", + "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "24.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz", + "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/pg": { + "version": "8.6.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", + "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/pg-pool": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz", + "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==", + "license": "MIT", + "dependencies": { + "@types/pg": "*" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", + "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", + "license": "MIT" + }, + "node_modules/@types/shimmer": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", + "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", + "license": "MIT" + }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "license": "MIT" + }, + "node_modules/@types/tedious": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", + "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, + "node_modules/@vercel/edge": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vercel/edge/-/edge-1.2.2.tgz", + "integrity": "sha512-1+y+f6rk0Yc9ss9bRDgz/gdpLimwoRteKHhrcgHvEpjbP1nyT3ByqEMWm2BTcpIO5UtDmIFXc8zdq4LR190PDA==", + "license": "Apache-2.0" + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/address": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/address/-/address-2.0.3.tgz", + "integrity": "sha512-XNAb/a6TCqou+TufU8/u11HCu9x1gYvOoxLwtlXgIqmkrYQADVv6ljyW2zwiPhHz9R1gItAWpuDrdJMmrOBFEA==", + "license": "MIT", + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/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==", + "license": "MIT" + }, + "node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "license": "MIT", + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-to-html": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", + "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", + "license": "MIT", + "dependencies": { + "entities": "^2.2.0" + }, + "bin": { + "ansi-to-html": "bin/ansi-to-html" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/astring": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", + "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", + "license": "MIT", + "bin": { + "astring": "bin/astring" + } + }, + "node_modules/babel-dead-code-elimination": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.9.tgz", + "integrity": "sha512-JLIhax/xullfInZjtu13UJjaLHDeTzt3vOeomaSUdO/nAMEL/pWC/laKrSvWylXMnVWyL5bpmG9njqBZlUQOdg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/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==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blueimp-md5": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/body-parser/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==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001707", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", + "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/castable-video": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/castable-video/-/castable-video-1.1.10.tgz", + "integrity": "sha512-/T1I0A4VG769wTEZ8gWuy1Crn9saAfRTd1UYTb8xbOPlN78+zOi/1nU2dD5koNkfE5VWvgabkIqrGKmyNXOjSQ==", + "license": "MIT", + "dependencies": { + "custom-media-element": "~1.4.5" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ce-la-react": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ce-la-react/-/ce-la-react-0.3.0.tgz", + "integrity": "sha512-84SEDLNHaAjykzlkqgKRq95hA3qnxrsTrwh4hTgBq6tfpINqajxz4bkz9q4orhUfpqDPQRgdCzYTF3bHcvTIlQ==", + "license": "BSD-3-Clause", + "peerDependencies": { + "react": ">=17.0.0" + } + }, + "node_modules/chai": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.1.tgz", + "integrity": "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A==", + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chai-dom": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/chai-dom/-/chai-dom-1.12.1.tgz", + "integrity": "sha512-tvz+D0PJue2VHXRec3udgP/OeeXBiePU3VH6JhEnHQJYzvNzR2nUvEykA9dXVS76JvaUENSOYH8Ufr0kZSnlCQ==", + "license": "MIT", + "engines": { + "node": ">= 0.12.0" + }, + "peerDependencies": { + "chai": ">= 3" + } + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "license": "MIT" + }, + "node_modules/clean-stack": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", + "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "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==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/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==", + "license": "MIT" + }, + "node_modules/cliui/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==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/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==", + "license": "MIT", + "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/close-with-grace": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/close-with-grace/-/close-with-grace-2.2.0.tgz", + "integrity": "sha512-OdcFxnxTm/AMLPHA4Aq3J1BLpkojXP7I4G5QBQLN5TT55ED/rk04rAoDbtfNnfZ988kGXPxh1bdRLeIU9bz/lA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/collapse-white-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", + "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.0.tgz", + "integrity": "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "license": "MIT" + }, + "node_modules/confetti-react": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/confetti-react/-/confetti-react-2.6.0.tgz", + "integrity": "sha512-1eBh8WZxLuXk4s7DlFcctNJETb6AbDxArJrotOoSdk8zFBmOw5Ey1O6ZM5wCOmBa+cG1Kd2YCC85u8oitocpbg==", + "license": "MIT", + "dependencies": { + "tween-functions": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.3.1" + } + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "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==", + "license": "MIT", + "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.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", + "license": "MIT" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", + "license": "MIT" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-gradient-parser": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.16.tgz", + "integrity": "sha512-3O5QdqgFRUbXvK1x5INf1YkBz1UKSWqrd63vWsum8MNHDBYD5urm3QtxZbKU259OrEXNM26lP/MPY3d1IGkBgA==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "license": "MIT" + }, + "node_modules/custom-media-element": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/custom-media-element/-/custom-media-element-1.4.5.tgz", + "integrity": "sha512-cjrsQufETwxjvwZbYbKBCJNvmQ2++G9AvT45zDi7NXL9k2PdVcs2h0jQz96J6G4TMKRCcEsoJ+QTgQD00Igtjw==", + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.32.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.32.0.tgz", + "integrity": "sha512-5JHBC9n75kz5851jeklCPmZWcg3hUe6sjqJvyk3+hVqFaKcHwHgxsjeN1yLmggoUc6STbtm9/NQyabQehfjvWQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz", + "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", + "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "license": "MIT" + }, + "node_modules/dompurify": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dotenv": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.0.1.tgz", + "integrity": "sha512-GLjkduuAL7IMJg/ZnOPm9AnWKJ82mSE2tzXLaJ/6hD6DhwGfZaXG77oB8qbReyiczNxnbxQKyh0OE5mXq0bAHA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.123", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.123.tgz", + "integrity": "sha512-refir3NlutEZqlKaBLK0tzlVLe5P2wDKS7UQt/3SpibizgsRAPOsqQC3ffw1nlv3ze5gjRQZYHoPymgVZkplFA==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/emoji-regex-xs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", + "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "license": "MIT" + }, + "node_modules/emoticon": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz", + "integrity": "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esast-util-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", + "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esast-util-from-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", + "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "acorn": "^8.0.0", + "esast-util-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esbuild": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estree-util-attach-comments": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", + "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-build-jsx": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", + "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-walker": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-scope": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", + "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-to-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", + "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "astring": "^1.8.0", + "source-map": "^0.7.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-value-to-estree": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.4.0.tgz", + "integrity": "sha512-Zlp+gxis+gCfK12d3Srl2PdX2ybsEA8ZYy6vQGVQTNNYLEGRQQ56XB64bjemN8kxIKXP1nC9ip4Z+ILy9LGzvQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-polyfill": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/event-target-polyfill/-/event-target-polyfill-0.0.4.tgz", + "integrity": "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ==", + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/execa": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz", + "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/express/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==", + "license": "MIT" + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "license": "MIT" + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fault": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", + "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", + "license": "MIT", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/finalhandler/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==", + "license": "MIT" + }, + "node_modules/find-process": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.11.tgz", + "integrity": "sha512-mAOh9gGk9WZ4ip5UjV0o6Vb4SrfnAmtsFNzkMRH9HQiFXVQnDyQFrSHTK5UoG6E+KV+s+cIznbtwpfN41l2nFA==", + "license": "MIT", + "dependencies": { + "chalk": "~4.1.2", + "commander": "^12.1.0", + "loglevel": "^1.9.2" + }, + "bin": { + "find-process": "bin/find-process.js" + } + }, + "node_modules/find-process/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/find-process/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "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/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fkill": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/fkill/-/fkill-9.0.0.tgz", + "integrity": "sha512-MdYSsbdCaIRjzo5edthZtWmEZVMfr1qrtYZUHIdO3swCE+CoZA8S5l0s4jDsYlTa9ZiXv0pTgpzE7s4N8NeUOA==", + "license": "MIT", + "dependencies": { + "aggregate-error": "^5.0.0", + "execa": "^8.0.1", + "pid-port": "^1.0.0", + "process-exists": "^5.0.0", + "ps-list": "^8.1.1", + "taskkill": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fkill/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fkill/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fkill/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/fkill/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fkill/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fkill/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fkill/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" + }, + "node_modules/framer-motion": { + "version": "12.23.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.0.tgz", + "integrity": "sha512-xf6NxTGAyf7zR4r2KlnhFmsRfKIbjqeBupEDBAaEtVIBJX96sAon00kMlsKButSIRwPSHjbRrAPnYdJJ9kyhbA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.22.0", + "motion-utils": "^12.19.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", + "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.0.3", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz", + "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.3", + "path-type": "^6.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphql": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", + "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-estree": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", + "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-attach-comments": "^3.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==", + "license": "MIT" + }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hls.js": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.6.tgz", + "integrity": "sha512-S4uTCwTHOtImW+/jxMjzG7udbHy5z682YQRbm/4f7VXuVNEoGBRjPJnD3Fxrufomdhzdtv24KnxRhPMXSvL6Fw==", + "license": "Apache-2.0" + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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==", + "license": "MIT", + "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/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/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==", + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-in-the-middle": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.14.2.tgz", + "integrity": "sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==", + "license": "Apache-2.0", + "dependencies": { + "acorn": "^8.14.0", + "acorn-import-attributes": "^1.9.5", + "cjs-module-lexer": "^1.2.2", + "module-details-from-path": "^1.0.3" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isbot": { + "version": "5.1.28", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.28.tgz", + "integrity": "sha512-qrOp4g3xj8YNse4biorv6O5ZShwsJM0trsoda4y7j/Su7ZtTTfVXFzbKkpgcSoDrHS8FcTuUwcU04YimZlZOxw==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jose": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz", + "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/katex": { + "version": "0.16.22", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", + "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "license": "MIT" + }, + "node_modules/langium": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", + "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, + "node_modules/local-pkg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz", + "integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==", + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.0.1", + "quansync": "^0.2.8" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz", + "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/markdown-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", + "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/match-sorter": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-8.0.0.tgz", + "integrity": "sha512-bGJ6Zb+OhzXe+ptP5d80OLVx7AkqfRbtGEh30vNSfjNwllu+hHI+tcbMIT/fbkx/FKN1PmKuDb65+Oofg+XUxw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5-hex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-5.0.0.tgz", + "integrity": "sha512-18TKd0nxBzMLflLBSCM/I9n50izl7NQGuujgbKjVUs/9acY+a5uzpDUVd4wV130vaK67TzDnPin2gze88u+e4Q==", + "license": "MIT", + "dependencies": { + "blueimp-md5": "^2.19.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-frontmatter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", + "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "escape-string-regexp": "^5.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdx-bundler": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/mdx-bundler/-/mdx-bundler-10.1.1.tgz", + "integrity": "sha512-87FtxC7miUPznwqEaAlJARinHJ6Qin9kDuG2E2BCCNEOszr62kHpqivI/IF/CmwObVSpvApVFFxN1ftM/Gykvw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@esbuild-plugins/node-resolve": "^0.2.2", + "@fal-works/esbuild-plugin-global-externals": "^2.1.2", + "@mdx-js/esbuild": "^3.0.0", + "gray-matter": "^4.0.3", + "remark-frontmatter": "^5.0.0", + "remark-mdx-frontmatter": "^4.0.0", + "uuid": "^9.0.1", + "vfile": "^6.0.1" + }, + "engines": { + "node": ">=18", + "npm": ">=6" + }, + "peerDependencies": { + "esbuild": "0.*" + } + }, + "node_modules/media-chrome": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.11.1.tgz", + "integrity": "sha512-+2niDc4qOwlpFAjwxg1OaizK/zKV6y7QqGm4nBFEVlSaG0ZBgOmfc4IXAPiirZqAlZGaFFUaMqCl1SpGU0/naA==", + "license": "MIT", + "dependencies": { + "@vercel/edge": "^1.2.1", + "ce-la-react": "^0.3.0" + } + }, + "node_modules/media-tracks": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/media-tracks/-/media-tracks-0.3.3.tgz", + "integrity": "sha512-9P2FuUHnZZ3iji+2RQk7Zkh5AmZTnOG5fODACnjhCVveX1McY3jmCRHofIEI+yTBqplz7LXy48c7fQ3Uigp88w==", + "license": "MIT" + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.8.1.tgz", + "integrity": "sha512-VSXJLqP1Sqw5sGr273mhvpPRhXwE6NlmMSqBZQw+yZJoAJkOIPPn/uT3teeCBx60Fkt5zEI3FrH2eVT0jXRDzw==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.0.4", + "@iconify/utils": "^2.1.33", + "@mermaid-js/parser": "^0.6.1", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.11", + "dayjs": "^1.11.13", + "dompurify": "^3.2.5", + "katex": "^0.16.9", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^15.0.7", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-frontmatter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", + "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", + "license": "MIT", + "dependencies": { + "fault": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/micromark/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==", + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "license": "ISC", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mlly": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/motion-dom": { + "version": "12.22.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.22.0.tgz", + "integrity": "sha512-ooH7+/BPw9gOsL9VtPhEJHE2m4ltnhMlcGMhEqA0YGNhKof7jdaszvsyThXI6LVIKshJUZ9/CP6HNqQhJfV7kw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.19.0" + } + }, + "node_modules/motion-utils": { + "version": "12.19.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.19.0.tgz", + "integrity": "sha512-BuFTHINYmV07pdWs6lj6aI63vr2N4dg0vR+td0rtrdpWOhBzIkEklZyLcvKBoEtwSqx8Jg06vUB5RS0xDiUybw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/msw": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.10.3.tgz", + "integrity": "sha512-rpqW4wIqISJlgDfu3tiqzuWC/d6jofSuMUsBu1rwepzSwX21aQoagsd+fjahJ8sewa6FwlYhu4no+jfGVQm2IA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.1", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^5.0.0", + "@mswjs/interceptors": "^0.39.1", + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.26.1", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/mux-embed": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/mux-embed/-/mux-embed-5.9.0.tgz", + "integrity": "sha512-wmunL3uoPhma/tWy8PrDPZkvJpXvSFBwbD3KkC4PG8Ztjfb1X3hRJwGUAQyRz7z99b/ovLm2UTTitrkvStjH4w==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-abi/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oauth4webapi": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.5.5.tgz", + "integrity": "sha512-1K88D2GiAydGblHo39NBro5TebGXa+7tYoyIbxvqv3+haDDry7CBE1eSYuNbOSsYCCU6y0gdynVZAkm4YPw4hg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oniguruma-parser": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", + "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz", + "integrity": "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.1", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/open": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openid-client": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-6.6.2.tgz", + "integrity": "sha512-Xya5TNMnnZuTM6DbHdB4q0S3ig2NTAELnii/ASie1xDEr8iiB8zZbO871OWBdrw++sd3hW6bqWjgcmSy1RTWHA==", + "license": "MIT", + "dependencies": { + "jose": "^6.0.11", + "oauth4webapi": "^3.5.4" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", + "integrity": "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.0.tgz", + "integrity": "sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.1", + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/package-manager-detector": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", + "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", + "license": "MIT" + }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-git-diff": { + "version": "0.0.19", + "resolved": "https://registry.npmjs.org/parse-git-diff/-/parse-git-diff-0.0.19.tgz", + "integrity": "sha512-oh3giwKzsPlOhekiDDyd/pfFKn04IZoTjEThquhfKigwiUHymiP/Tp6AN5nGIwXQdWuBTQvz9AaRdN5TBsJ8MA==", + "license": "MIT" + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", + "license": "ISC" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/partysocket": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-1.1.4.tgz", + "integrity": "sha512-jXP7PFj2h5/v4UjDS8P7MZy6NJUQ7sspiFyxL4uc/+oKOL+KdtXzHnTV8INPGxBrLTXgalyG3kd12Qm7WrYc3A==", + "license": "ISC", + "dependencies": { + "event-target-polyfill": "^0.0.4" + } + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", + "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", + "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pid-port": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pid-port/-/pid-port-1.0.2.tgz", + "integrity": "sha512-Khqp07zX8IJpmIg56bHrLxS3M0iSL4cq6wnMq8YE7r/hSw3Kn4QxYS6QJg8Bs22Z7CSVj7eSsxFuigYVIFWmjg==", + "license": "MIT", + "dependencies": { + "execa": "^8.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pid-port/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/pid-port/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pid-port/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/pid-port/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pid-port/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pid-port/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pid-port/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz", + "integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/player.style": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/player.style/-/player.style-0.1.9.tgz", + "integrity": "sha512-aFmIhHMrnAP8YliFYFMnRw+5AlHqBvnqWy4vHGo2kFxlC+XjmTXqgg62qSxlE8ubAY83c0ViEZGYglSJi6mGCA==", + "license": "MIT", + "workspaces": [ + ".", + "site", + "examples/*", + "scripts/*", + "themes/*" + ], + "dependencies": { + "media-chrome": "~4.11.0" + } + }, + "node_modules/playwright": { + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.54.1.tgz", + "integrity": "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.54.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.54.1.tgz", + "integrity": "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" + }, + "node_modules/pretty-ms": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", + "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-exists/-/process-exists-5.0.0.tgz", + "integrity": "sha512-6QPRh5fyHD8MaXr4GYML8K/YY0Sq5dKHGIOrAKS3cYpHQdmygFCcijIu1dVoNKAZ0TWAMoeh8KDK9dF8auBkJA==", + "license": "MIT", + "dependencies": { + "ps-list": "^8.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/ps-list": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/ps-list/-/ps-list-8.1.1.tgz", + "integrity": "sha512-OPS9kEJYVmiO48u/B9qneqhkMvgCxT+Tm28VCEJpheTpl8cJ0ffZRRNgS5mrQRTrX5yRTpaJ+hRDeefXYmmorQ==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/quansync": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", + "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.0.tgz", + "integrity": "sha512-3FUYSwlvB/5wRJVTL/aavqHmfUKe0+Xm9MllkYgGo9eDwNdkvwlJGjpPxono1kCycLt6AnDTgjmXvK3/B4QGuw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/recma-build-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", + "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-build-jsx": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-jsx": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.0.tgz", + "integrity": "sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==", + "license": "MIT", + "dependencies": { + "acorn-jsx": "^5.0.0", + "estree-util-to-js": "^2.0.0", + "recma-parse": "^1.0.0", + "recma-stringify": "^1.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", + "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "esast-util-from-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/recma-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", + "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-util-to-js": "^2.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redent/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.0.1.tgz", + "integrity": "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", + "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "rehype-parse": "^9.0.0", + "rehype-stringify": "^10.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-autolink-headings": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz", + "integrity": "sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-recma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", + "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "hast-util-to-estree": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", + "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-emoji": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-5.0.1.tgz", + "integrity": "sha512-QCqTSvcZ65Ym+P+VyBKd4JfJfh7icMl7cIOGVmPMzWkDtdD8pQ0nQG7yxGolVIiMzSx90EZ7SwNiVpYpfTxn7w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.4", + "emoticon": "^4.0.1", + "mdast-util-find-and-replace": "^3.0.1", + "node-emoji": "^2.1.3", + "unified": "^11.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/remark-frontmatter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", + "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-frontmatter": "^2.0.0", + "micromark-extension-frontmatter": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.0.tgz", + "integrity": "sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx-frontmatter": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-mdx-frontmatter/-/remark-mdx-frontmatter-4.0.0.tgz", + "integrity": "sha512-PZzAiDGOEfv1Ua7exQ8S5kKxkD8CDaSb4nM+1Mprs6u8dyvQifakh+kCj6NovfGXW+bTvrhjaR3srzjS2qJHKg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "estree-util-value-to-estree": "^3.0.0", + "toml": "^3.0.0", + "unified": "^11.0.0", + "yaml": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remix-flat-routes": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/remix-flat-routes/-/remix-flat-routes-0.8.5.tgz", + "integrity": "sha512-30GcEpvwqFXCyTKiCTeqI3QSNyTg+f0qLGeIc95y6o3gaOEIhbC37qWpe8HrVEkdnj48xUyaUK03jm1zYFkhfA==", + "license": "MIT", + "dependencies": { + "fs-extra": "^11.2.0", + "minimatch": "^10.0.1" + }, + "bin": { + "migrate-flat-routes": "dist/cli.cjs" + }, + "engines": { + "node": ">=16.6.0" + } + }, + "node_modules/remix-utils": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/remix-utils/-/remix-utils-8.7.0.tgz", + "integrity": "sha512-oRumofsYFaxHyPtqLuYe3g2nQi4SMYjCoebaeed0gYHIOKBiPPYdNP6cgmQbFjQQ5pwXV+uQiKLqO6pM9ep3VA==", + "funding": [ + "https://github.com/sponsors/sergiodxa" + ], + "license": "MIT", + "dependencies": { + "type-fest": "^4.37.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@edgefirst-dev/batcher": "^1.0.0", + "@edgefirst-dev/jwt": "^1.2.0", + "@edgefirst-dev/server-timing": "^0.0.1", + "@oslojs/crypto": "^1.0.1", + "@oslojs/encoding": "^1.1.0", + "intl-parse-accept-language": "^1.0.0", + "is-ip": "^5.0.1", + "react": "^18.0.0 || ^19.0.0", + "react-router": "^7.0.0", + "zod": "^3.22.4" + }, + "peerDependenciesMeta": { + "@edgefirst-dev/batcher": { + "optional": true + }, + "@edgefirst-dev/jwt": { + "optional": true + }, + "@edgefirst-dev/server-timing": { + "optional": true + }, + "@oslojs/crypto": { + "optional": true + }, + "@oslojs/encoding": { + "optional": true + }, + "intl-parse-accept-language": { + "optional": true + }, + "is-ip": { + "optional": true + }, + "react": { + "optional": true + }, + "react-router": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-in-the-middle": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", + "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3", + "resolve": "^1.22.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/require-in-the-middle/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/require-in-the-middle/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==", + "license": "MIT" + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/router/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==", + "license": "MIT" + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "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" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/satori": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.15.2.tgz", + "integrity": "sha512-vu/49vdc8MzV5jUchs3TIRDCOkOvMc1iJ11MrZvhg9tE4ziKIEIBjBZvies6a9sfM2vQ2gc3dXeu6rCK7AztHA==", + "license": "MPL-2.0", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-gradient-parser": "^0.0.16", + "css-to-react-native": "^3.0.0", + "emoji-regex-xs": "^2.0.1", + "escape-html": "^1.0.3", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-wasm-web": "^0.3.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/send/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shiki": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.8.1.tgz", + "integrity": "sha512-+MYIyjwGPCaegbpBeFN9+oOifI8CKiKG3awI/6h3JeT85c//H2wDW/xCJEGuQ5jPqtbboKNqNy+JyX9PYpGwNg==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "3.8.1", + "@shikijs/engine-javascript": "3.8.1", + "@shikijs/engine-oniguruma": "3.8.1", + "@shikijs/langs": "3.8.1", + "@shikijs/themes": "3.8.1", + "@shikijs/types": "3.8.1", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "license": "BSD-2-Clause" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sonner": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.6.tgz", + "integrity": "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/spin-delay": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/spin-delay/-/spin-delay-2.0.1.tgz", + "integrity": "sha512-ilggKXKqAMwk21PSYvxuF/KCnrsGFDrnO6mXa629mj8fvfo+dOQfubDViqsRjRX5U1jd3Xb8FTsV+m4Tg7YeUg==", + "license": "MIT", + "peerDependencies": { + "react": ">=17.0.1" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "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==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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==", + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/style-to-js": { + "version": "1.1.17", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz", + "integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.9" + } + }, + "node_modules/style-to-object": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", + "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/taskkill": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/taskkill/-/taskkill-5.0.0.tgz", + "integrity": "sha512-+HRtZ40Vc+6YfCDWCeAsixwxJgMbPY4HHuTgzPYH3JXvqHWUlsCfy+ylXlAKhFNcuLp4xVeWeFBUhDk+7KYUvQ==", + "license": "MIT", + "dependencies": { + "execa": "^6.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/taskkill/node_modules/execa": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", + "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^3.0.1", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/taskkill/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/taskkill/node_modules/human-signals": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", + "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/taskkill/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/taskkill/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/taskkill/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/taskkill/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/taskkill/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tween-functions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz", + "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==", + "license": "BSD" + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "license": "MIT" + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unplugin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz", + "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.8.1", + "chokidar": "^3.5.3", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.5.0" + } + }, + "node_modules/unplugin/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/unplugin/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite-env-only": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vite-env-only/-/vite-env-only-3.0.3.tgz", + "integrity": "sha512-iAb7cTXRrvFShaF1n+G8f6Yqq7sRJcxipNYNQQu0DN5N9P55vJMmLG5lNU5moYGpd+ZH1WhBHdkWi5WjrfImHg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/generator": "^7.23.6", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6", + "babel-dead-code-elimination": "^1.0.6", + "micromatch": "^4.0.5" + }, + "peerDependencies": { + "vite": "*" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack-sources": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", + "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", + "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "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==", + "license": "MIT", + "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/wrap-ansi-cjs/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/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==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "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==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/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==", + "license": "MIT" + }, + "node_modules/yargs/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==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoga-wasm-web": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", + "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.25.75", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.75.tgz", + "integrity": "sha512-OhpzAmVzabPOL6C3A3gpAifqr9MqihV/Msx3gor2b2kviCgcb+HM9SEOpMWwwNp9MRunWnhtAKUoo0AHhjyPPg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/epicshop/package.json b/epicshop/package.json new file mode 100644 index 0000000..458fe6f --- /dev/null +++ b/epicshop/package.json @@ -0,0 +1,15 @@ +{ + "type": "module", + "scripts": { + "test": "node test.js" + }, + "dependencies": { + "@epic-web/workshop-app": "^6.19.5", + "enquirer": "^2.4.1", + "execa": "^9.5.2", + "match-sorter": "^8.0.0", + "p-limit": "^6.2.0", + "@epic-web/workshop-cli": "^6.19.5", + "@epic-web/workshop-utils": "^6.19.5" + } +} diff --git a/epicshop/setup-custom.js b/epicshop/setup-custom.js new file mode 100644 index 0000000..3181cc3 --- /dev/null +++ b/epicshop/setup-custom.js @@ -0,0 +1,48 @@ +import path from 'node:path' +import { warm } from '@epic-web/workshop-cli/warm' +import { + getApps, + isProblemApp, + setPlayground, +} from '@epic-web/workshop-utils/apps.server' +import { $ } from 'execa' +import fsExtra from 'fs-extra' + +await warm() + +const allApps = await getApps() +const problemApps = allApps.filter(isProblemApp) + +if (!process.env.SKIP_PLAYGROUND) { + const firstProblemApp = problemApps[0] + if (firstProblemApp) { + console.log('๐Ÿ› setting up the first problem app...') + const playgroundPath = path.join(process.cwd(), 'playground') + if (await fsExtra.exists(playgroundPath)) { + console.log('๐Ÿ—‘ deleting existing playground app') + await fsExtra.remove(playgroundPath) + } + await setPlayground(firstProblemApp.fullPath).then( + () => { + console.log('โœ… first problem app set up') + }, + (error) => { + console.error(error) + throw new Error('โŒ first problem app setup failed') + }, + ) + } +} + +if (!process.env.SKIP_PLAYWRIGHT) { + console.log( + '๐ŸŽญ installing playwright for testing... This may require sudo (or admin) privileges and may ask for your password. It will also take some time depending on whether you installed recently or have a slower network connection... Thanks for your patience!', + ) + try { + await $`npx playwright install chromium --with-deps` + console.log('โœ… playwright installed') + } catch (error) { + console.error('โŒ playwright install failed:', error.message) + throw error + } +} diff --git a/kcdshop/setup.js b/epicshop/setup.js similarity index 100% rename from kcdshop/setup.js rename to epicshop/setup.js diff --git a/epicshop/test.js b/epicshop/test.js new file mode 100644 index 0000000..e4ecff8 --- /dev/null +++ b/epicshop/test.js @@ -0,0 +1,272 @@ +import path from 'node:path' +import { performance } from 'perf_hooks' +import { fileURLToPath } from 'url' +import { + getApps, + getAppDisplayName, +} from '@epic-web/workshop-utils/apps.server' +import enquirer from 'enquirer' +import { execa } from 'execa' +import { matchSorter } from 'match-sorter' +import pLimit from 'p-limit' + +const { prompt } = enquirer + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +function captureOutput() { + const output = [] + return { + write: (chunk, streamType) => { + output.push({ chunk: chunk.toString(), streamType }) + }, + replay: () => { + for (const { chunk, streamType } of output) { + if (streamType === 'stderr') { + process.stderr.write(chunk) + } else { + process.stdout.write(chunk) + } + } + }, + hasOutput: () => output.length > 0, + } +} + +function printTestSummary(results) { + const label = '--- Test Summary ---' + console.log(`\n${label}`) + for (const [appPath, { result, duration }] of results) { + let emoji + switch (result) { + case 'Passed': + emoji = 'โœ…' + break + case 'Failed': + emoji = 'โŒ' + break + case 'Error': + emoji = '๐Ÿ’ฅ' + break + case 'Incomplete': + emoji = 'โณ' + break + default: + emoji = 'โ“' + } + console.log(`${emoji} ${appPath} (${duration.toFixed(2)}s)`) + } + console.log(`${'-'.repeat(label.length)}\n`) +} + +async function main() { + const allApps = await getApps() + + let selectedApps + let additionalArgs = [] + + // Parse command-line arguments + const argIndex = process.argv.indexOf('--') + if (argIndex !== -1) { + additionalArgs = process.argv.slice(argIndex + 1) + process.argv = process.argv.slice(0, argIndex) + } + + if (process.argv[2]) { + const patterns = process.argv[2].toLowerCase().split(',') + selectedApps = allApps.filter((app) => { + const { exerciseNumber, stepNumber, type } = app + + return patterns.some((pattern) => { + let [patternExercise = '*', patternStep = '*', patternType = '*'] = + pattern.split('.') + + patternExercise ||= '*' + patternStep ||= '*' + patternType ||= '*' + + return ( + (patternExercise === '*' || + exerciseNumber === Number(patternExercise)) && + (patternStep === '*' || stepNumber === Number(patternStep)) && + (patternType === '*' || type.includes(patternType)) + ) + }) + }) + } else { + const displayNameMap = new Map( + allApps.map((app) => [getAppDisplayName(app, allApps), app]), + ) + const choices = displayNameMap.keys() + + const response = await prompt({ + type: 'autocomplete', + name: 'appDisplayNames', + message: 'Select apps to test:', + choices: ['All', ...choices], + multiple: true, + suggest: (input, choices) => { + return matchSorter(choices, input, { keys: ['name'] }) + }, + }) + + selectedApps = response.appDisplayNames.includes('All') + ? allApps + : response.appDisplayNames.map((appDisplayName) => + displayNameMap.get(appDisplayName), + ) + + // Update this block to use process.argv + const appPattern = + selectedApps.length === allApps.length + ? '*' + : selectedApps + .map((app) => `${app.exerciseNumber}.${app.stepNumber}.${app.type}`) + .join(',') + const additionalArgsString = + additionalArgs.length > 0 ? ` -- ${additionalArgs.join(' ')}` : '' + console.log(`\nโ„น๏ธ To skip the prompt next time, use this command:`) + console.log(`npm test -- ${appPattern}${additionalArgsString}\n`) + } + + if (selectedApps.length === 0) { + console.log('โš ๏ธ No apps selected. Exiting.') + return + } + + if (selectedApps.length === 1) { + const app = selectedApps[0] + console.log(`๐Ÿš€ Running tests for ${app.relativePath}\n\n`) + const startTime = performance.now() + try { + await execa('npm', ['run', 'test', '--silent', '--', ...additionalArgs], { + cwd: app.fullPath, + stdio: 'inherit', + env: { + ...process.env, + PORT: app.dev.portNumber, + }, + }) + const duration = (performance.now() - startTime) / 1000 + console.log( + `โœ… Finished tests for ${app.relativePath} (${duration.toFixed(2)}s)`, + ) + } catch { + const duration = (performance.now() - startTime) / 1000 + console.error( + `โŒ Tests failed for ${app.relativePath} (${duration.toFixed(2)}s)`, + ) + process.exit(1) + } + } else { + const limit = pLimit(1) + let hasFailures = false + const runningProcesses = new Map() + let isShuttingDown = false + const results = new Map() + + const shutdownHandler = () => { + if (isShuttingDown) return + isShuttingDown = true + console.log('\nGracefully shutting down. Please wait...') + console.log('Outputting results of running tests:') + for (const [app, output] of runningProcesses.entries()) { + if (output.hasOutput()) { + console.log(`\nPartial results for ${app.relativePath}:\n\n`) + output.replay() + console.log('\n\n') + } else { + console.log(`โ„น๏ธ No output captured for ${app.relativePath}`) + } + // Set result for incomplete tests + if (!results.has(app.relativePath)) { + results.set(app.relativePath, 'Incomplete') + } + } + printTestSummary(results) + // Allow some time for output to be written before exiting + setTimeout(() => process.exit(1), 100) + } + + process.on('SIGINT', shutdownHandler) + process.on('SIGTERM', shutdownHandler) + + const tasks = selectedApps.map((app) => + limit(async () => { + if (isShuttingDown) return + console.log(`๐Ÿš€ Starting tests for ${app.relativePath}`) + const output = captureOutput() + runningProcesses.set(app, output) + const startTime = performance.now() + try { + const subprocess = execa( + 'npm', + ['run', 'test', '--silent', '--', ...additionalArgs], + { + cwd: path.join(__dirname, '..', app.relativePath), + reject: false, + env: { + ...process.env, + PORT: app.dev.portNumber, + }, + }, + ) + + subprocess.stdout.on('data', (chunk) => output.write(chunk, 'stdout')) + subprocess.stderr.on('data', (chunk) => output.write(chunk, 'stderr')) + + const { exitCode } = await subprocess + const duration = (performance.now() - startTime) / 1000 + + runningProcesses.delete(app) + + if (exitCode !== 0) { + hasFailures = true + console.error( + `\nโŒ Tests failed for ${app.relativePath} (${duration.toFixed(2)}s):\n\n`, + ) + output.replay() + console.log('\n\n') + results.set(app.relativePath, { result: 'Failed', duration }) + // Set result for incomplete tests + if (!results.has(app.relativePath)) { + results.set(app.relativePath, 'Incomplete') + } + } else { + console.log( + `โœ… Finished tests for ${app.relativePath} (${duration.toFixed(2)}s)`, + ) + results.set(app.relativePath, { result: 'Passed', duration }) + } + } catch (error) { + const duration = (performance.now() - startTime) / 1000 + runningProcesses.delete(app) + hasFailures = true + console.error( + `\nโŒ An error occurred while running tests for ${app.relativePath} (${duration.toFixed(2)}s):\n\n`, + ) + console.error(error.message) + output.replay() + console.log('\n\n') + results.set(app.relativePath, { result: 'Error', duration }) + } + }), + ) + + await Promise.all(tasks) + + // Print summary output + printTestSummary(results) + + if (hasFailures) { + process.exit(1) + } + } +} + +main().catch((error) => { + if (error) { + console.error('โŒ An error occurred:', error) + } + setTimeout(() => process.exit(1), 100) +}) diff --git a/epicshop/update-deps.sh b/epicshop/update-deps.sh new file mode 100755 index 0000000..1e3267d --- /dev/null +++ b/epicshop/update-deps.sh @@ -0,0 +1,7 @@ +npx npm-check-updates --dep prod,dev --upgrade --root +cd epicshop && npx npm-check-updates --dep prod,dev --upgrade --root +cd .. +rm -rf node_modules package-lock.json ./epicshop/package-lock.json ./epicshop/node_modules ./exercises/**/node_modules +npm install +npm run setup +npm run lint -- --fix diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..48f5c69 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,4 @@ +import defaultConfig from '@epic-web/config/eslint' + +/** @type {import("eslint").Linter.Config} */ +export default [...defaultConfig] diff --git a/exercises/01.exercises/01.problem.ssr/.prettierignore b/exercises/01.exercises/01.problem.ssr/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/01.problem.ssr/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/01.problem.ssr/.prettierrc b/exercises/01.exercises/01.problem.ssr/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/01.problem.ssr/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/01.problem.ssr/README.mdx b/exercises/01.exercises/01.problem.ssr/README.mdx deleted file mode 100644 index 56ab4c4..0000000 --- a/exercises/01.exercises/01.problem.ssr/README.mdx +++ /dev/null @@ -1,3 +0,0 @@ -# Server-Side Rendering - -Problem: Server render the UI diff --git a/exercises/01.exercises/01.problem.ssr/db/ship-api.js b/exercises/01.exercises/01.problem.ssr/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/01.problem.ssr/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/01.problem.ssr/db/ships.json b/exercises/01.exercises/01.problem.ssr/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/01.problem.ssr/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/01.problem.ssr/dev.js b/exercises/01.exercises/01.problem.ssr/dev.js deleted file mode 100644 index 3d986ec..0000000 --- a/exercises/01.exercises/01.problem.ssr/dev.js +++ /dev/null @@ -1,42 +0,0 @@ -import { spawn } from 'node:child_process' -import chalk from 'chalk' -import closeWithGrace from 'close-with-grace' -import getPort 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 ssrServer = spawnScript( - 'node', - ['--watch', 'server/ssr.js'], - { PORT: SSR_PORT }, - chalk.blue.bgBlack('SSR'), -) - -closeWithGrace(async () => { - console.log('Shutting down servers...') - const ssrExit = new Promise(resolve => ssrServer.on('exit', resolve)) - - ssrServer.kill('SIGTERM') - - await Promise.all([ssrExit]) -}) diff --git a/exercises/01.exercises/01.problem.ssr/package-lock.json b/exercises/01.exercises/01.problem.ssr/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/01.problem.ssr/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/01.problem.ssr/package.json b/exercises/01.exercises/01.problem.ssr/package.json deleted file mode 100644 index 0d03b2d..0000000 --- a/exercises/01.exercises/01.problem.ssr/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__01.problem.ssr", - "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/01.exercises/01.problem.ssr/public/favicon.ico b/exercises/01.exercises/01.problem.ssr/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/01.problem.ssr/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/01.problem.ssr/public/favicon.svg b/exercises/01.exercises/01.problem.ssr/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/01.problem.ssr/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/01.problem.ssr/server/ssr.js b/exercises/01.exercises/01.problem.ssr/server/ssr.js deleted file mode 100644 index df4e0e9..0000000 --- a/exercises/01.exercises/01.problem.ssr/server/ssr.js +++ /dev/null @@ -1,57 +0,0 @@ -import closeWithGrace from 'close-with-grace' -import compress from 'compression' -import express from 'express' -// ๐Ÿจ you're gonna want these -// import { renderToPipeableStream } from 'react-dom/server' -// import { getShip, searchShips } from '../db/ship-api.js' -// import { Document } from '../src/app.js' - -const PORT = process.env.PORT || 3000 - -const app = express() - -app.use(compress()) - -app.head('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) - -app.get('/', async function (req, res) { - try { - // ๐Ÿ’ฃ delete this first - throw new Error('not implemented yet') - - // ๐Ÿจ create a variable for the shipId ('6c86fca8b9086') and search ('') - // ๐Ÿจ get the ship using getShip and shipResults from searchShips - // ๐Ÿจ set the response Content-Type header to 'text/html' - // ๐Ÿ“œ https://expressjs.com/en/api.html#res.set - // ๐Ÿจ call renderToPipeableStream from react-dom/server with the Document element - // while passing the shipId, search, ship, and shipResults as props - // ๐Ÿ’ฐ remember, we don't have JSX in this workshop since this is an - // uncompiled JavaScript file, so you'll need to use `createElement`. - // ๐Ÿ“œ https://react.dev/reference/react-dom/server/renderToPipeableStream - // ๐Ÿจ with the returned object from renderToPipeableStream, call pipe(res) - // to pipe the streamed output into the response object - // ๐Ÿ“œ https://react.dev/reference/react-dom/server/renderToPipeableStream#rendering-a-react-tree-as-html-to-a-nodejs-stream - } catch (e) { - console.error(`Failed to SSR: ${e.stack}`) - res.statusCode = 500 - res.end(`Failed to SSR: ${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/01.exercises/01.problem.ssr/src/app.js b/exercises/01.exercises/01.problem.ssr/src/app.js deleted file mode 100644 index dcd4ff5..0000000 --- a/exercises/01.exercises/01.problem.ssr/src/app.js +++ /dev/null @@ -1,64 +0,0 @@ -import { Fragment, createElement as h } from 'react' -import { ShipDetails } from './ship-details.js' -import { SearchResults } from './ship-search-results.js' - -export function Document({ shipId, search, ship, shipResults }) { - 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, { shipId, search, ship, shipResults }), - ), - ), - ) -} - -export function App({ shipId, search, ship, shipResults }) { - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - defaultValue: search, - autoFocus: true, - }), - h('ul', null, h(SearchResults, { shipId, search, shipResults })), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? h(ShipDetails, { shipId, ship }) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/01.problem.ssr/src/img-utils.js b/exercises/01.exercises/01.problem.ssr/src/img-utils.js deleted file mode 100644 index 84fe730..0000000 --- a/exercises/01.exercises/01.problem.ssr/src/img-utils.js +++ /dev/null @@ -1,3 +0,0 @@ -export function getImageUrlForShip(shipId, { size }) { - return `/img/ships/${shipId}.webp?size=${size}` -} diff --git a/exercises/01.exercises/01.problem.ssr/src/ship-details.js b/exercises/01.exercises/01.problem.ssr/src/ship-details.js deleted file mode 100644 index 583b8a2..0000000 --- a/exercises/01.exercises/01.problem.ssr/src/ship-details.js +++ /dev/null @@ -1,43 +0,0 @@ -import { createElement as h } from 'react' -import { getImageUrlForShip } from './img-utils.js' - -export function ShipDetails({ ship }) { - const shipImgSrc = getImageUrlForShip(ship.id, { size: 200 }) - return h( - 'div', - { className: 'ship-info' }, - h( - 'div', - { className: 'ship-info__img-wrapper' }, - h('img', { 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.'), - ), - ) -} diff --git a/exercises/01.exercises/01.problem.ssr/src/ship-search-results.js b/exercises/01.exercises/01.problem.ssr/src/ship-search-results.js deleted file mode 100644 index 196dd49..0000000 --- a/exercises/01.exercises/01.problem.ssr/src/ship-search-results.js +++ /dev/null @@ -1,29 +0,0 @@ -import { createElement as h } from 'react' -import { getImageUrlForShip } from './img-utils.js' - -export function SearchResults({ shipId: currentShipId, shipResults, search }) { - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - src: getImageUrlForShip(ship.id, { size: 20 }), - alt: ship.name, - }), - ship.name, - ), - ) - }) -} diff --git a/exercises/01.exercises/01.solution.ssr/.prettierignore b/exercises/01.exercises/01.solution.ssr/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/01.solution.ssr/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/01.solution.ssr/.prettierrc b/exercises/01.exercises/01.solution.ssr/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/01.solution.ssr/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/01.solution.ssr/README.mdx b/exercises/01.exercises/01.solution.ssr/README.mdx deleted file mode 100644 index 1e94b86..0000000 --- a/exercises/01.exercises/01.solution.ssr/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# Server-Side Rendering diff --git a/exercises/01.exercises/01.solution.ssr/db/ship-api.js b/exercises/01.exercises/01.solution.ssr/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/01.solution.ssr/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/01.solution.ssr/db/ships.json b/exercises/01.exercises/01.solution.ssr/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/01.solution.ssr/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/01.solution.ssr/dev.js b/exercises/01.exercises/01.solution.ssr/dev.js deleted file mode 100644 index 3d986ec..0000000 --- a/exercises/01.exercises/01.solution.ssr/dev.js +++ /dev/null @@ -1,42 +0,0 @@ -import { spawn } from 'node:child_process' -import chalk from 'chalk' -import closeWithGrace from 'close-with-grace' -import getPort 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 ssrServer = spawnScript( - 'node', - ['--watch', 'server/ssr.js'], - { PORT: SSR_PORT }, - chalk.blue.bgBlack('SSR'), -) - -closeWithGrace(async () => { - console.log('Shutting down servers...') - const ssrExit = new Promise(resolve => ssrServer.on('exit', resolve)) - - ssrServer.kill('SIGTERM') - - await Promise.all([ssrExit]) -}) diff --git a/exercises/01.exercises/01.solution.ssr/package-lock.json b/exercises/01.exercises/01.solution.ssr/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/01.solution.ssr/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/01.solution.ssr/package.json b/exercises/01.exercises/01.solution.ssr/package.json deleted file mode 100644 index 4abe357..0000000 --- a/exercises/01.exercises/01.solution.ssr/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__01.solution.ssr", - "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/01.exercises/01.solution.ssr/public/favicon.ico b/exercises/01.exercises/01.solution.ssr/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/01.solution.ssr/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/01.solution.ssr/public/favicon.svg b/exercises/01.exercises/01.solution.ssr/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/01.solution.ssr/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/01.solution.ssr/server/ssr.js b/exercises/01.exercises/01.solution.ssr/server/ssr.js deleted file mode 100644 index fbe1d28..0000000 --- a/exercises/01.exercises/01.solution.ssr/server/ssr.js +++ /dev/null @@ -1,51 +0,0 @@ -import closeWithGrace from 'close-with-grace' -import compress from 'compression' -import express from 'express' -import { createElement as h } from 'react' -import { renderToPipeableStream } from 'react-dom/server' -import { getShip, searchShips } from '../db/ship-api.js' -import { Document } from '../src/app.js' - -const PORT = process.env.PORT || 3000 - -const app = express() - -app.use(compress()) - -app.head('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) - -app.get('/', async function (req, res) { - try { - const shipId = '6c86fca8b9086' - const search = '' - const ship = await getShip({ shipId }) - const shipResults = await searchShips({ search }) - res.set('Content-type', 'text/html') - const { pipe } = renderToPipeableStream( - h(Document, { shipId, search, ship, shipResults }), - ) - pipe(res) - } catch (e) { - console.error(`Failed to SSR: ${e.stack}`) - res.statusCode = 500 - res.end(`Failed to SSR: ${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/01.exercises/01.solution.ssr/src/app.js b/exercises/01.exercises/01.solution.ssr/src/app.js deleted file mode 100644 index dcd4ff5..0000000 --- a/exercises/01.exercises/01.solution.ssr/src/app.js +++ /dev/null @@ -1,64 +0,0 @@ -import { Fragment, createElement as h } from 'react' -import { ShipDetails } from './ship-details.js' -import { SearchResults } from './ship-search-results.js' - -export function Document({ shipId, search, ship, shipResults }) { - 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, { shipId, search, ship, shipResults }), - ), - ), - ) -} - -export function App({ shipId, search, ship, shipResults }) { - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - defaultValue: search, - autoFocus: true, - }), - h('ul', null, h(SearchResults, { shipId, search, shipResults })), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? h(ShipDetails, { shipId, ship }) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/01.solution.ssr/src/img-utils.js b/exercises/01.exercises/01.solution.ssr/src/img-utils.js deleted file mode 100644 index 84fe730..0000000 --- a/exercises/01.exercises/01.solution.ssr/src/img-utils.js +++ /dev/null @@ -1,3 +0,0 @@ -export function getImageUrlForShip(shipId, { size }) { - return `/img/ships/${shipId}.webp?size=${size}` -} diff --git a/exercises/01.exercises/01.solution.ssr/src/ship-details.js b/exercises/01.exercises/01.solution.ssr/src/ship-details.js deleted file mode 100644 index 583b8a2..0000000 --- a/exercises/01.exercises/01.solution.ssr/src/ship-details.js +++ /dev/null @@ -1,43 +0,0 @@ -import { createElement as h } from 'react' -import { getImageUrlForShip } from './img-utils.js' - -export function ShipDetails({ ship }) { - const shipImgSrc = getImageUrlForShip(ship.id, { size: 200 }) - return h( - 'div', - { className: 'ship-info' }, - h( - 'div', - { className: 'ship-info__img-wrapper' }, - h('img', { 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.'), - ), - ) -} diff --git a/exercises/01.exercises/01.solution.ssr/src/ship-search-results.js b/exercises/01.exercises/01.solution.ssr/src/ship-search-results.js deleted file mode 100644 index 196dd49..0000000 --- a/exercises/01.exercises/01.solution.ssr/src/ship-search-results.js +++ /dev/null @@ -1,29 +0,0 @@ -import { createElement as h } from 'react' -import { getImageUrlForShip } from './img-utils.js' - -export function SearchResults({ shipId: currentShipId, shipResults, search }) { - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - src: getImageUrlForShip(ship.id, { size: 20 }), - alt: ship.name, - }), - ship.name, - ), - ) - }) -} diff --git a/exercises/01.exercises/02.problem.server-context/.prettierignore b/exercises/01.exercises/02.problem.server-context/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/02.problem.server-context/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/02.problem.server-context/.prettierrc b/exercises/01.exercises/02.problem.server-context/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/02.problem.server-context/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/02.problem.server-context/README.mdx b/exercises/01.exercises/02.problem.server-context/README.mdx deleted file mode 100644 index c8ac9d7..0000000 --- a/exercises/01.exercises/02.problem.server-context/README.mdx +++ /dev/null @@ -1,9 +0,0 @@ -# Server Context - -Problem: Prop drilling everything - -We're going to create the -[`AsyncLocalStorage`](https://nodejs.org/api/async_context.html) object -in (click to create the file). In -there you'll create an `AsyncLocalStorage` object called `shipDataStorage` and -`export` it. diff --git a/exercises/01.exercises/02.problem.server-context/db/ship-api.js b/exercises/01.exercises/02.problem.server-context/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/02.problem.server-context/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/02.problem.server-context/db/ships.json b/exercises/01.exercises/02.problem.server-context/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/02.problem.server-context/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/02.problem.server-context/dev.js b/exercises/01.exercises/02.problem.server-context/dev.js deleted file mode 100644 index 3d986ec..0000000 --- a/exercises/01.exercises/02.problem.server-context/dev.js +++ /dev/null @@ -1,42 +0,0 @@ -import { spawn } from 'node:child_process' -import chalk from 'chalk' -import closeWithGrace from 'close-with-grace' -import getPort 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 ssrServer = spawnScript( - 'node', - ['--watch', 'server/ssr.js'], - { PORT: SSR_PORT }, - chalk.blue.bgBlack('SSR'), -) - -closeWithGrace(async () => { - console.log('Shutting down servers...') - const ssrExit = new Promise(resolve => ssrServer.on('exit', resolve)) - - ssrServer.kill('SIGTERM') - - await Promise.all([ssrExit]) -}) diff --git a/exercises/01.exercises/02.problem.server-context/package-lock.json b/exercises/01.exercises/02.problem.server-context/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/02.problem.server-context/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/02.problem.server-context/package.json b/exercises/01.exercises/02.problem.server-context/package.json deleted file mode 100644 index 4fc00a4..0000000 --- a/exercises/01.exercises/02.problem.server-context/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__02.problem.server-context", - "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/01.exercises/02.problem.server-context/public/favicon.ico b/exercises/01.exercises/02.problem.server-context/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/02.problem.server-context/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/02.problem.server-context/public/favicon.svg b/exercises/01.exercises/02.problem.server-context/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/02.problem.server-context/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/02.problem.server-context/server/ssr.js b/exercises/01.exercises/02.problem.server-context/server/ssr.js deleted file mode 100644 index c837c76..0000000 --- a/exercises/01.exercises/02.problem.server-context/server/ssr.js +++ /dev/null @@ -1,56 +0,0 @@ -import closeWithGrace from 'close-with-grace' -import compress from 'compression' -import express from 'express' -import { createElement as h } from 'react' -import { renderToPipeableStream } from 'react-dom/server' -import { getShip, searchShips } from '../db/ship-api.js' -import { Document } from '../src/app.js' -// ๐Ÿจ import the shipDataStorage from ./async-storage.js here - -const PORT = process.env.PORT || 3000 - -const app = express() - -app.use(compress()) - -app.head('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) - -app.get('/', async function (req, res) { - try { - const shipId = '6c86fca8b9086' - const search = '' - const ship = await getShip({ shipId }) - const shipResults = await searchShips({ search }) - res.set('Content-type', 'text/html') - // ๐Ÿจ wrap this bit in shipDataStorage.run and pass the shipId, search, - // ship, and shipResults - const { pipe } = renderToPipeableStream( - // ๐Ÿ’ฃ remove these props (components will access this through the - // shipDataStorage instead). - h(Document, { shipId, search, ship, shipResults }), - ) - pipe(res) - } catch (e) { - console.error(`Failed to SSR: ${e.stack}`) - res.statusCode = 500 - res.end(`Failed to SSR: ${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/01.exercises/02.problem.server-context/src/app.js b/exercises/01.exercises/02.problem.server-context/src/app.js deleted file mode 100644 index 9ec4d64..0000000 --- a/exercises/01.exercises/02.problem.server-context/src/app.js +++ /dev/null @@ -1,87 +0,0 @@ -import { Fragment, createElement as h } from 'react' -// ๐Ÿจ import the shipDataStorage from ../server/async-storage.js here -import { ShipDetails } from './ship-details.js' -import { SearchResults } from './ship-search-results.js' - -export function Document( - // ๐Ÿ’ฃ remove these props - { shipId, search, ship, shipResults }, -) { - 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, - // ๐Ÿ’ฃ remove these props - { shipId, search, ship, shipResults }, - ), - ), - ), - ) -} - -export function App( - // ๐Ÿ’ฃ remove these props - { shipId, search, ship, shipResults }, -) { - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - defaultValue: search, - autoFocus: true, - }), - h( - 'ul', - null, - h( - SearchResults, - // ๐Ÿ’ฃ remove these props - { shipId, search, shipResults }, - ), - ), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? h( - ShipDetails, - // ๐Ÿ’ฃ remove these props - { shipId, ship }, - ) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/02.problem.server-context/src/img-utils.js b/exercises/01.exercises/02.problem.server-context/src/img-utils.js deleted file mode 100644 index 84fe730..0000000 --- a/exercises/01.exercises/02.problem.server-context/src/img-utils.js +++ /dev/null @@ -1,3 +0,0 @@ -export function getImageUrlForShip(shipId, { size }) { - return `/img/ships/${shipId}.webp?size=${size}` -} diff --git a/exercises/01.exercises/02.problem.server-context/src/ship-details.js b/exercises/01.exercises/02.problem.server-context/src/ship-details.js deleted file mode 100644 index 8092f6b..0000000 --- a/exercises/01.exercises/02.problem.server-context/src/ship-details.js +++ /dev/null @@ -1,48 +0,0 @@ -import { createElement as h } from 'react' -// ๐Ÿจ get your shipDataStorage from ../server/async-storage.js -import { getImageUrlForShip } from './img-utils.js' - -export function ShipDetails( - // ๐Ÿ’ฃ remove this prop - { ship }, -) { - // ๐Ÿจ get the ship from shipDataStorage.getStore() - const shipImgSrc = getImageUrlForShip(ship.id, { size: 200 }) - return h( - 'div', - { className: 'ship-info' }, - h( - 'div', - { className: 'ship-info__img-wrapper' }, - h('img', { 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.'), - ), - ) -} diff --git a/exercises/01.exercises/02.problem.server-context/src/ship-search-results.js b/exercises/01.exercises/02.problem.server-context/src/ship-search-results.js deleted file mode 100644 index ce74f96..0000000 --- a/exercises/01.exercises/02.problem.server-context/src/ship-search-results.js +++ /dev/null @@ -1,34 +0,0 @@ -import { createElement as h } from 'react' -// ๐Ÿจ get your shipDataStorage from ../server/async-storage.js -import { getImageUrlForShip } from './img-utils.js' - -export function SearchResults( - // ๐Ÿ’ฃ remove these props - { shipId: currentShipId, shipResults, search }, -) { - // ๐Ÿจ get the shipId, shipResults, and search from shipDataStorage.getStore() - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - src: getImageUrlForShip(ship.id, { size: 20 }), - alt: ship.name, - }), - ship.name, - ), - ) - }) -} diff --git a/exercises/01.exercises/02.solution.server-context/.prettierignore b/exercises/01.exercises/02.solution.server-context/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/02.solution.server-context/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/02.solution.server-context/.prettierrc b/exercises/01.exercises/02.solution.server-context/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/02.solution.server-context/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/02.solution.server-context/README.mdx b/exercises/01.exercises/02.solution.server-context/README.mdx deleted file mode 100644 index a5e173c..0000000 --- a/exercises/01.exercises/02.solution.server-context/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# Server Context diff --git a/exercises/01.exercises/02.solution.server-context/db/ship-api.js b/exercises/01.exercises/02.solution.server-context/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/02.solution.server-context/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/02.solution.server-context/db/ships.json b/exercises/01.exercises/02.solution.server-context/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/02.solution.server-context/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/02.solution.server-context/dev.js b/exercises/01.exercises/02.solution.server-context/dev.js deleted file mode 100644 index 3d986ec..0000000 --- a/exercises/01.exercises/02.solution.server-context/dev.js +++ /dev/null @@ -1,42 +0,0 @@ -import { spawn } from 'node:child_process' -import chalk from 'chalk' -import closeWithGrace from 'close-with-grace' -import getPort 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 ssrServer = spawnScript( - 'node', - ['--watch', 'server/ssr.js'], - { PORT: SSR_PORT }, - chalk.blue.bgBlack('SSR'), -) - -closeWithGrace(async () => { - console.log('Shutting down servers...') - const ssrExit = new Promise(resolve => ssrServer.on('exit', resolve)) - - ssrServer.kill('SIGTERM') - - await Promise.all([ssrExit]) -}) diff --git a/exercises/01.exercises/02.solution.server-context/package-lock.json b/exercises/01.exercises/02.solution.server-context/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/02.solution.server-context/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/02.solution.server-context/package.json b/exercises/01.exercises/02.solution.server-context/package.json deleted file mode 100644 index d13aefe..0000000 --- a/exercises/01.exercises/02.solution.server-context/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__02.solution.server-context", - "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/01.exercises/02.solution.server-context/public/favicon.ico b/exercises/01.exercises/02.solution.server-context/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/02.solution.server-context/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/02.solution.server-context/public/favicon.svg b/exercises/01.exercises/02.solution.server-context/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/02.solution.server-context/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/02.solution.server-context/server/ssr.js b/exercises/01.exercises/02.solution.server-context/server/ssr.js deleted file mode 100644 index c1d4584..0000000 --- a/exercises/01.exercises/02.solution.server-context/server/ssr.js +++ /dev/null @@ -1,53 +0,0 @@ -import closeWithGrace from 'close-with-grace' -import compress from 'compression' -import express from 'express' -import { createElement as h } from 'react' -import { renderToPipeableStream } from 'react-dom/server' -import { getShip, searchShips } from '../db/ship-api.js' -import { Document } from '../src/app.js' -import { shipDataStorage } from './async-storage.js' - -const PORT = process.env.PORT || 3000 - -const app = express() - -app.use(compress()) - -app.head('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) - -app.get('/', async function (req, res) { - try { - const shipId = '6c86fca8b9086' - const search = '' - const ship = await getShip({ shipId }) - const shipResults = await searchShips({ search }) - res.set('Content-type', 'text/html') - shipDataStorage.run({ shipId, search, ship, shipResults }, () => { - const root = h(Document) - const { pipe } = renderToPipeableStream(root) - pipe(res) - }) - } catch (e) { - console.error(`Failed to SSR: ${e.stack}`) - res.statusCode = 500 - res.end(`Failed to SSR: ${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/01.exercises/02.solution.server-context/src/app.js b/exercises/01.exercises/02.solution.server-context/src/app.js deleted file mode 100644 index cb7333f..0000000 --- a/exercises/01.exercises/02.solution.server-context/src/app.js +++ /dev/null @@ -1,58 +0,0 @@ -import { Fragment, createElement as h } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { ShipDetails } from './ship-details.js' -import { SearchResults } from './ship-search-results.js' - -export 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))), - ) -} - -function App() { - const { shipId, search } = shipDataStorage.getStore() - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - defaultValue: search, - autoFocus: true, - }), - h('ul', null, h(SearchResults)), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? h(ShipDetails) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/02.solution.server-context/src/img-utils.js b/exercises/01.exercises/02.solution.server-context/src/img-utils.js deleted file mode 100644 index 84fe730..0000000 --- a/exercises/01.exercises/02.solution.server-context/src/img-utils.js +++ /dev/null @@ -1,3 +0,0 @@ -export function getImageUrlForShip(shipId, { size }) { - return `/img/ships/${shipId}.webp?size=${size}` -} diff --git a/exercises/01.exercises/02.solution.server-context/src/ship-details.js b/exercises/01.exercises/02.solution.server-context/src/ship-details.js deleted file mode 100644 index 47b6ab9..0000000 --- a/exercises/01.exercises/02.solution.server-context/src/ship-details.js +++ /dev/null @@ -1,45 +0,0 @@ -import { createElement as h } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { getImageUrlForShip } from './img-utils.js' - -export function ShipDetails() { - const { ship } = shipDataStorage.getStore() - const shipImgSrc = getImageUrlForShip(ship.id, { size: 200 }) - return h( - 'div', - { className: 'ship-info' }, - h( - 'div', - { className: 'ship-info__img-wrapper' }, - h('img', { 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.'), - ), - ) -} diff --git a/exercises/01.exercises/02.solution.server-context/src/ship-search-results.js b/exercises/01.exercises/02.solution.server-context/src/ship-search-results.js deleted file mode 100644 index 61aa94d..0000000 --- a/exercises/01.exercises/02.solution.server-context/src/ship-search-results.js +++ /dev/null @@ -1,35 +0,0 @@ -import { createElement as h } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { getImageUrlForShip } from './img-utils.js' - -export function SearchResults() { - const { - shipId: currentShipId, - shipResults, - search, - } = shipDataStorage.getStore() - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - src: getImageUrlForShip(ship.id, { size: 20 }), - alt: ship.name, - }), - ship.name, - ), - ) - }) -} diff --git a/exercises/01.exercises/03.problem.url/.prettierignore b/exercises/01.exercises/03.problem.url/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/03.problem.url/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/03.problem.url/.prettierrc b/exercises/01.exercises/03.problem.url/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/03.problem.url/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/03.problem.url/README.mdx b/exercises/01.exercises/03.problem.url/README.mdx deleted file mode 100644 index d985d67..0000000 --- a/exercises/01.exercises/03.problem.url/README.mdx +++ /dev/null @@ -1,4 +0,0 @@ -# URL State - -Problem: Drive the state of the rendered HTML by the URL and support form -submission for search. diff --git a/exercises/01.exercises/03.problem.url/db/ship-api.js b/exercises/01.exercises/03.problem.url/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/03.problem.url/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/03.problem.url/db/ships.json b/exercises/01.exercises/03.problem.url/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/03.problem.url/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/03.problem.url/dev.js b/exercises/01.exercises/03.problem.url/dev.js deleted file mode 100644 index 3d986ec..0000000 --- a/exercises/01.exercises/03.problem.url/dev.js +++ /dev/null @@ -1,42 +0,0 @@ -import { spawn } from 'node:child_process' -import chalk from 'chalk' -import closeWithGrace from 'close-with-grace' -import getPort 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 ssrServer = spawnScript( - 'node', - ['--watch', 'server/ssr.js'], - { PORT: SSR_PORT }, - chalk.blue.bgBlack('SSR'), -) - -closeWithGrace(async () => { - console.log('Shutting down servers...') - const ssrExit = new Promise(resolve => ssrServer.on('exit', resolve)) - - ssrServer.kill('SIGTERM') - - await Promise.all([ssrExit]) -}) diff --git a/exercises/01.exercises/03.problem.url/package-lock.json b/exercises/01.exercises/03.problem.url/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/03.problem.url/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/03.problem.url/package.json b/exercises/01.exercises/03.problem.url/package.json deleted file mode 100644 index 85e6661..0000000 --- a/exercises/01.exercises/03.problem.url/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__03.problem.url", - "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/01.exercises/03.problem.url/public/favicon.ico b/exercises/01.exercises/03.problem.url/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/03.problem.url/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/03.problem.url/public/favicon.svg b/exercises/01.exercises/03.problem.url/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/03.problem.url/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/03.problem.url/server/ssr.js b/exercises/01.exercises/03.problem.url/server/ssr.js deleted file mode 100644 index a253c8f..0000000 --- a/exercises/01.exercises/03.problem.url/server/ssr.js +++ /dev/null @@ -1,57 +0,0 @@ -import closeWithGrace from 'close-with-grace' -import compress from 'compression' -import express from 'express' -import { createElement as h } from 'react' -import { renderToPipeableStream } from 'react-dom/server' -import { getShip, searchShips } from '../db/ship-api.js' -import { Document } from '../src/app.js' -import { shipDataStorage } from './async-storage.js' - -const PORT = process.env.PORT || 3000 - -const app = express() - -app.use(compress()) - -app.head('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) - -// ๐Ÿจ update this url to support an optional shipId param -// ๐Ÿ’ฐ /:shipId? -app.get('/', async function (req, res) { - try { - // ๐Ÿจ get the shipId from req.params.shipId (fallback to null) - const shipId = '6c86fca8b9086' - // ๐Ÿจ get the search from req.query.search (fallback to '') - const search = '' - const ship = await getShip({ shipId }) - const shipResults = await searchShips({ search }) - res.set('Content-type', 'text/html') - shipDataStorage.run({ shipId, search, ship, shipResults }, () => { - const root = h(Document) - const { pipe } = renderToPipeableStream(root) - pipe(res) - }) - } catch (e) { - console.error(`Failed to SSR: ${e.stack}`) - res.statusCode = 500 - res.end(`Failed to SSR: ${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/01.exercises/03.problem.url/src/app.js b/exercises/01.exercises/03.problem.url/src/app.js deleted file mode 100644 index 7a99aba..0000000 --- a/exercises/01.exercises/03.problem.url/src/app.js +++ /dev/null @@ -1,60 +0,0 @@ -import { Fragment, createElement as h } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { ShipDetails } from './ship-details.js' -import { SearchResults } from './ship-search-results.js' - -export 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))), - ) -} - -function App() { - const { shipId, search } = shipDataStorage.getStore() - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - // ๐Ÿจ wrap this in a form element so it will submit when you hit "enter" - // in the input field. - h( - Fragment, - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - defaultValue: search, - autoFocus: true, - }), - h('ul', null, h(SearchResults)), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? h(ShipDetails) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/03.problem.url/src/img-utils.js b/exercises/01.exercises/03.problem.url/src/img-utils.js deleted file mode 100644 index 84fe730..0000000 --- a/exercises/01.exercises/03.problem.url/src/img-utils.js +++ /dev/null @@ -1,3 +0,0 @@ -export function getImageUrlForShip(shipId, { size }) { - return `/img/ships/${shipId}.webp?size=${size}` -} diff --git a/exercises/01.exercises/03.problem.url/src/ship-details.js b/exercises/01.exercises/03.problem.url/src/ship-details.js deleted file mode 100644 index 47b6ab9..0000000 --- a/exercises/01.exercises/03.problem.url/src/ship-details.js +++ /dev/null @@ -1,45 +0,0 @@ -import { createElement as h } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { getImageUrlForShip } from './img-utils.js' - -export function ShipDetails() { - const { ship } = shipDataStorage.getStore() - const shipImgSrc = getImageUrlForShip(ship.id, { size: 200 }) - return h( - 'div', - { className: 'ship-info' }, - h( - 'div', - { className: 'ship-info__img-wrapper' }, - h('img', { 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.'), - ), - ) -} diff --git a/exercises/01.exercises/03.problem.url/src/ship-search-results.js b/exercises/01.exercises/03.problem.url/src/ship-search-results.js deleted file mode 100644 index 61aa94d..0000000 --- a/exercises/01.exercises/03.problem.url/src/ship-search-results.js +++ /dev/null @@ -1,35 +0,0 @@ -import { createElement as h } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { getImageUrlForShip } from './img-utils.js' - -export function SearchResults() { - const { - shipId: currentShipId, - shipResults, - search, - } = shipDataStorage.getStore() - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - src: getImageUrlForShip(ship.id, { size: 20 }), - alt: ship.name, - }), - ship.name, - ), - ) - }) -} diff --git a/exercises/01.exercises/03.solution.url/.prettierignore b/exercises/01.exercises/03.solution.url/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/03.solution.url/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/03.solution.url/.prettierrc b/exercises/01.exercises/03.solution.url/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/03.solution.url/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/03.solution.url/README.mdx b/exercises/01.exercises/03.solution.url/README.mdx deleted file mode 100644 index 27d4d98..0000000 --- a/exercises/01.exercises/03.solution.url/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# URL State diff --git a/exercises/01.exercises/03.solution.url/db/ship-api.js b/exercises/01.exercises/03.solution.url/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/03.solution.url/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/03.solution.url/db/ships.json b/exercises/01.exercises/03.solution.url/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/03.solution.url/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/03.solution.url/dev.js b/exercises/01.exercises/03.solution.url/dev.js deleted file mode 100644 index 3d986ec..0000000 --- a/exercises/01.exercises/03.solution.url/dev.js +++ /dev/null @@ -1,42 +0,0 @@ -import { spawn } from 'node:child_process' -import chalk from 'chalk' -import closeWithGrace from 'close-with-grace' -import getPort 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 ssrServer = spawnScript( - 'node', - ['--watch', 'server/ssr.js'], - { PORT: SSR_PORT }, - chalk.blue.bgBlack('SSR'), -) - -closeWithGrace(async () => { - console.log('Shutting down servers...') - const ssrExit = new Promise(resolve => ssrServer.on('exit', resolve)) - - ssrServer.kill('SIGTERM') - - await Promise.all([ssrExit]) -}) diff --git a/exercises/01.exercises/03.solution.url/package-lock.json b/exercises/01.exercises/03.solution.url/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/03.solution.url/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/03.solution.url/package.json b/exercises/01.exercises/03.solution.url/package.json deleted file mode 100644 index 9ef148c..0000000 --- a/exercises/01.exercises/03.solution.url/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__03.solution.url", - "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/01.exercises/03.solution.url/public/favicon.ico b/exercises/01.exercises/03.solution.url/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/03.solution.url/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/03.solution.url/public/favicon.svg b/exercises/01.exercises/03.solution.url/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/03.solution.url/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/03.solution.url/server/ssr.js b/exercises/01.exercises/03.solution.url/server/ssr.js deleted file mode 100644 index 0a88ad6..0000000 --- a/exercises/01.exercises/03.solution.url/server/ssr.js +++ /dev/null @@ -1,53 +0,0 @@ -import closeWithGrace from 'close-with-grace' -import compress from 'compression' -import express from 'express' -import { createElement as h } from 'react' -import { renderToPipeableStream } from 'react-dom/server' -import { getShip, searchShips } from '../db/ship-api.js' -import { Document } from '../src/app.js' -import { shipDataStorage } from './async-storage.js' - -const PORT = process.env.PORT || 3000 - -const app = express() - -app.use(compress()) - -app.head('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) - -app.get('/:shipId?', async function (req, res) { - try { - const shipId = req.params.shipId || null - const search = req.query.search || '' - const ship = shipId ? await getShip({ shipId }) : null - const shipResults = await searchShips({ search }) - res.set('Content-type', 'text/html') - shipDataStorage.run({ shipId, search, ship, shipResults }, () => { - const root = h(Document) - const { pipe } = renderToPipeableStream(root) - pipe(res) - }) - } catch (e) { - console.error(`Failed to SSR: ${e.stack}`) - res.statusCode = 500 - res.end(`Failed to SSR: ${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/01.exercises/03.solution.url/src/app.js b/exercises/01.exercises/03.solution.url/src/app.js deleted file mode 100644 index 3d441e3..0000000 --- a/exercises/01.exercises/03.solution.url/src/app.js +++ /dev/null @@ -1,63 +0,0 @@ -import { Fragment, createElement as h } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { ShipDetails } from './ship-details.js' -import { SearchResults } from './ship-search-results.js' - -export 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))), - ) -} - -function App() { - const { shipId, search } = shipDataStorage.getStore() - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h( - 'form', - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - name: 'search', - defaultValue: search, - autoFocus: true, - }), - ), - h('ul', null, h(SearchResults)), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? h(ShipDetails) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/03.solution.url/src/img-utils.js b/exercises/01.exercises/03.solution.url/src/img-utils.js deleted file mode 100644 index 84fe730..0000000 --- a/exercises/01.exercises/03.solution.url/src/img-utils.js +++ /dev/null @@ -1,3 +0,0 @@ -export function getImageUrlForShip(shipId, { size }) { - return `/img/ships/${shipId}.webp?size=${size}` -} diff --git a/exercises/01.exercises/03.solution.url/src/ship-details.js b/exercises/01.exercises/03.solution.url/src/ship-details.js deleted file mode 100644 index 47b6ab9..0000000 --- a/exercises/01.exercises/03.solution.url/src/ship-details.js +++ /dev/null @@ -1,45 +0,0 @@ -import { createElement as h } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { getImageUrlForShip } from './img-utils.js' - -export function ShipDetails() { - const { ship } = shipDataStorage.getStore() - const shipImgSrc = getImageUrlForShip(ship.id, { size: 200 }) - return h( - 'div', - { className: 'ship-info' }, - h( - 'div', - { className: 'ship-info__img-wrapper' }, - h('img', { 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.'), - ), - ) -} diff --git a/exercises/01.exercises/03.solution.url/src/ship-search-results.js b/exercises/01.exercises/03.solution.url/src/ship-search-results.js deleted file mode 100644 index 61aa94d..0000000 --- a/exercises/01.exercises/03.solution.url/src/ship-search-results.js +++ /dev/null @@ -1,35 +0,0 @@ -import { createElement as h } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { getImageUrlForShip } from './img-utils.js' - -export function SearchResults() { - const { - shipId: currentShipId, - shipResults, - search, - } = shipDataStorage.getStore() - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - src: getImageUrlForShip(ship.id, { size: 20 }), - alt: ship.name, - }), - ship.name, - ), - ) - }) -} diff --git a/exercises/01.exercises/04.problem.async-components/.prettierignore b/exercises/01.exercises/04.problem.async-components/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/04.problem.async-components/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/04.problem.async-components/.prettierrc b/exercises/01.exercises/04.problem.async-components/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/04.problem.async-components/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/04.problem.async-components/README.mdx b/exercises/01.exercises/04.problem.async-components/README.mdx deleted file mode 100644 index 81dc43d..0000000 --- a/exercises/01.exercises/04.problem.async-components/README.mdx +++ /dev/null @@ -1,10 +0,0 @@ -# Async Components - -Problem: Send UI when it's ready rather than waiting for everything to be done. - - - Once you have the async components streaming, you should take a look at the - HTML the server is sending. You'll find some interesting hidden divs and some - inline script tags. These are used to put the streamed-in content in the right - place! - diff --git a/exercises/01.exercises/04.problem.async-components/db/ship-api.js b/exercises/01.exercises/04.problem.async-components/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/04.problem.async-components/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/04.problem.async-components/db/ships.json b/exercises/01.exercises/04.problem.async-components/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/04.problem.async-components/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/04.problem.async-components/dev.js b/exercises/01.exercises/04.problem.async-components/dev.js deleted file mode 100644 index 3d986ec..0000000 --- a/exercises/01.exercises/04.problem.async-components/dev.js +++ /dev/null @@ -1,42 +0,0 @@ -import { spawn } from 'node:child_process' -import chalk from 'chalk' -import closeWithGrace from 'close-with-grace' -import getPort 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 ssrServer = spawnScript( - 'node', - ['--watch', 'server/ssr.js'], - { PORT: SSR_PORT }, - chalk.blue.bgBlack('SSR'), -) - -closeWithGrace(async () => { - console.log('Shutting down servers...') - const ssrExit = new Promise(resolve => ssrServer.on('exit', resolve)) - - ssrServer.kill('SIGTERM') - - await Promise.all([ssrExit]) -}) diff --git a/exercises/01.exercises/04.problem.async-components/package-lock.json b/exercises/01.exercises/04.problem.async-components/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/04.problem.async-components/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/04.problem.async-components/package.json b/exercises/01.exercises/04.problem.async-components/package.json deleted file mode 100644 index 76e8ceb..0000000 --- a/exercises/01.exercises/04.problem.async-components/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__04.problem.async-components", - "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/01.exercises/04.problem.async-components/public/favicon.ico b/exercises/01.exercises/04.problem.async-components/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/04.problem.async-components/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/04.problem.async-components/public/favicon.svg b/exercises/01.exercises/04.problem.async-components/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/04.problem.async-components/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/04.problem.async-components/server/ssr.js b/exercises/01.exercises/04.problem.async-components/server/ssr.js deleted file mode 100644 index db6c4f2..0000000 --- a/exercises/01.exercises/04.problem.async-components/server/ssr.js +++ /dev/null @@ -1,55 +0,0 @@ -import closeWithGrace from 'close-with-grace' -import compress from 'compression' -import express from 'express' -import { createElement as h } from 'react' -import { renderToPipeableStream } from 'react-dom/server' -import { getShip, searchShips } from '../db/ship-api.js' -import { Document } from '../src/app.js' -import { shipDataStorage } from './async-storage.js' - -const PORT = process.env.PORT || 3000 - -const app = express() - -app.use(compress()) - -app.head('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) - -app.get('/:shipId?', async function (req, res) { - try { - const shipId = req.params.shipId || null - const search = req.query.search || '' - // ๐Ÿ’ฃ delete ship and shipResults - const ship = shipId ? await getShip({ shipId }) : null - const shipResults = await searchShips({ search }) - res.set('Content-type', 'text/html') - // ๐Ÿ’ฃ remove ship and shipResults from this context - shipDataStorage.run({ shipId, search, ship, shipResults }, () => { - const root = h(Document) - const { pipe } = renderToPipeableStream(root) - pipe(res) - }) - } catch (e) { - console.error(`Failed to SSR: ${e.stack}`) - res.statusCode = 500 - res.end(`Failed to SSR: ${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/01.exercises/04.problem.async-components/src/app.js b/exercises/01.exercises/04.problem.async-components/src/app.js deleted file mode 100644 index b7bb204..0000000 --- a/exercises/01.exercises/04.problem.async-components/src/app.js +++ /dev/null @@ -1,72 +0,0 @@ -import { Fragment, createElement as h } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -// ๐Ÿจ bring in ShipFallback here -import { ShipDetails } from './ship-details.js' -// ๐Ÿจ bring in SearchResultsFallback here -import { SearchResults } from './ship-search-results.js' - -// ๐Ÿจ make this component async to signal to React this tree should be streamed -export 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))), - ) -} - -function App() { - const { shipId, search } = shipDataStorage.getStore() - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h( - 'form', - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - name: 'search', - defaultValue: search, - autoFocus: true, - }), - ), - h( - 'ul', - null, - // ๐Ÿจ wrap this in Suspense using h(SearchResultsFallback) as the fallback - h(SearchResults), - ), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? // ๐Ÿจ wrap this in Suspense using h(ShipFallback) as the fallback - h(ShipDetails) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/04.problem.async-components/src/ship-details.js b/exercises/01.exercises/04.problem.async-components/src/ship-details.js deleted file mode 100644 index a4fab4e..0000000 --- a/exercises/01.exercises/04.problem.async-components/src/ship-details.js +++ /dev/null @@ -1,85 +0,0 @@ -import { createElement as h } from 'react' -// ๐Ÿจ get the getShip util from ../db/ship-api.js here -import { shipDataStorage } from '../server/async-storage.js' -import { getImageUrlForShip } from './img-utils.js' - -export function ShipDetails() { - // ๐Ÿจ instead of the ship, get the shipId from storage - const { ship } = shipDataStorage.getStore() - // ๐Ÿจ get the ship by calling getShip with { shipId } here - const shipImgSrc = getImageUrlForShip(ship.id, { size: 200 }) - return h( - 'div', - { className: 'ship-info' }, - h( - 'div', - { className: 'ship-info__img-wrapper' }, - h('img', { 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('img', { - 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)')), - ), - ), - ), - ), - ) -} diff --git a/exercises/01.exercises/04.problem.async-components/src/ship-search-results.js b/exercises/01.exercises/04.problem.async-components/src/ship-search-results.js deleted file mode 100644 index 7849b18..0000000 --- a/exercises/01.exercises/04.problem.async-components/src/ship-search-results.js +++ /dev/null @@ -1,55 +0,0 @@ -import { createElement as h } from 'react' -// ๐Ÿจ get searchShips from ../db/ship-api.js here -import { shipDataStorage } from '../server/async-storage.js' -import { getImageUrlForShip, shipFallbackSrc } from './img-utils.js' - -export function SearchResults() { - const { - shipId: currentShipId, - // ๐Ÿ’ฃ delete the shipResults here - shipResults, - search, - } = shipDataStorage.getStore() - // ๐Ÿจ get shipResults by calling searchShips with { search } - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - 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/01.exercises/04.solution.async-components/.prettierignore b/exercises/01.exercises/04.solution.async-components/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/04.solution.async-components/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/04.solution.async-components/.prettierrc b/exercises/01.exercises/04.solution.async-components/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/04.solution.async-components/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/04.solution.async-components/README.mdx b/exercises/01.exercises/04.solution.async-components/README.mdx deleted file mode 100644 index c4f66d3..0000000 --- a/exercises/01.exercises/04.solution.async-components/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# Async Components diff --git a/exercises/01.exercises/04.solution.async-components/db/ship-api.js b/exercises/01.exercises/04.solution.async-components/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/04.solution.async-components/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/04.solution.async-components/db/ships.json b/exercises/01.exercises/04.solution.async-components/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/04.solution.async-components/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/04.solution.async-components/dev.js b/exercises/01.exercises/04.solution.async-components/dev.js deleted file mode 100644 index 3d986ec..0000000 --- a/exercises/01.exercises/04.solution.async-components/dev.js +++ /dev/null @@ -1,42 +0,0 @@ -import { spawn } from 'node:child_process' -import chalk from 'chalk' -import closeWithGrace from 'close-with-grace' -import getPort 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 ssrServer = spawnScript( - 'node', - ['--watch', 'server/ssr.js'], - { PORT: SSR_PORT }, - chalk.blue.bgBlack('SSR'), -) - -closeWithGrace(async () => { - console.log('Shutting down servers...') - const ssrExit = new Promise(resolve => ssrServer.on('exit', resolve)) - - ssrServer.kill('SIGTERM') - - await Promise.all([ssrExit]) -}) diff --git a/exercises/01.exercises/04.solution.async-components/package-lock.json b/exercises/01.exercises/04.solution.async-components/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/04.solution.async-components/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/04.solution.async-components/package.json b/exercises/01.exercises/04.solution.async-components/package.json deleted file mode 100644 index b4ced23..0000000 --- a/exercises/01.exercises/04.solution.async-components/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__04.solution.async-components", - "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/01.exercises/04.solution.async-components/public/favicon.ico b/exercises/01.exercises/04.solution.async-components/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/04.solution.async-components/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/04.solution.async-components/public/favicon.svg b/exercises/01.exercises/04.solution.async-components/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/04.solution.async-components/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/04.solution.async-components/server/ssr.js b/exercises/01.exercises/04.solution.async-components/server/ssr.js deleted file mode 100644 index 0962c17..0000000 --- a/exercises/01.exercises/04.solution.async-components/server/ssr.js +++ /dev/null @@ -1,50 +0,0 @@ -import closeWithGrace from 'close-with-grace' -import compress from 'compression' -import express from 'express' -import { createElement as h } from 'react' -import { renderToPipeableStream } from 'react-dom/server' -import { Document } from '../src/app.js' -import { shipDataStorage } from './async-storage.js' - -const PORT = process.env.PORT || 3000 - -const app = express() - -app.use(compress()) - -app.head('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) - -app.get('/:shipId?', async function (req, res) { - try { - const shipId = req.params.shipId || null - const search = req.query.search || '' - res.set('Content-type', 'text/html') - shipDataStorage.run({ shipId, search }, () => { - const root = h(Document) - const { pipe } = renderToPipeableStream(root) - pipe(res) - }) - } catch (e) { - console.error(`Failed to SSR: ${e.stack}`) - res.statusCode = 500 - res.end(`Failed to SSR: ${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/01.exercises/04.solution.async-components/src/app.js b/exercises/01.exercises/04.solution.async-components/src/app.js deleted file mode 100644 index 90cbc73..0000000 --- a/exercises/01.exercises/04.solution.async-components/src/app.js +++ /dev/null @@ -1,67 +0,0 @@ -import { Fragment, createElement as h, Suspense } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { ShipDetails, ShipFallback } from './ship-details.js' -import { SearchResults, SearchResultsFallback } from './ship-search-results.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))), - ) -} - -function App() { - const { shipId, search } = shipDataStorage.getStore() - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h( - 'form', - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - name: 'search', - defaultValue: search, - autoFocus: true, - }), - ), - h( - 'ul', - null, - h(Suspense, { fallback: h(SearchResultsFallback) }, h(SearchResults)), - ), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? h(Suspense, { fallback: h(ShipFallback) }, h(ShipDetails)) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/04.solution.async-components/src/ship-details.js b/exercises/01.exercises/04.solution.async-components/src/ship-details.js deleted file mode 100644 index a8b4b30..0000000 --- a/exercises/01.exercises/04.solution.async-components/src/ship-details.js +++ /dev/null @@ -1,84 +0,0 @@ -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' - -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('img', { 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('img', { - 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)')), - ), - ), - ), - ), - ) -} diff --git a/exercises/01.exercises/04.solution.async-components/src/ship-search-results.js b/exercises/01.exercises/04.solution.async-components/src/ship-search-results.js deleted file mode 100644 index b686499..0000000 --- a/exercises/01.exercises/04.solution.async-components/src/ship-search-results.js +++ /dev/null @@ -1,50 +0,0 @@ -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' - -export async function SearchResults() { - const { shipId: currentShipId, search } = shipDataStorage.getStore() - const shipResults = await searchShips({ search }) - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - 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/01.exercises/05.problem.bootstrap/.prettierignore b/exercises/01.exercises/05.problem.bootstrap/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/05.problem.bootstrap/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/05.problem.bootstrap/.prettierrc b/exercises/01.exercises/05.problem.bootstrap/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/05.problem.bootstrap/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/05.problem.bootstrap/README.mdx b/exercises/01.exercises/05.problem.bootstrap/README.mdx deleted file mode 100644 index 5d4eb5c..0000000 --- a/exercises/01.exercises/05.problem.bootstrap/README.mdx +++ /dev/null @@ -1,7 +0,0 @@ -# Bootstrap Modules - -Problem: Get client-side JavaScript on the page - -You're going to create a file at (click that -to create the file), and add a `console.log` in it to be sure it's being loaded -properly when you're finished. diff --git a/exercises/01.exercises/05.problem.bootstrap/db/ship-api.js b/exercises/01.exercises/05.problem.bootstrap/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/05.problem.bootstrap/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/05.problem.bootstrap/db/ships.json b/exercises/01.exercises/05.problem.bootstrap/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/05.problem.bootstrap/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/05.problem.bootstrap/dev.js b/exercises/01.exercises/05.problem.bootstrap/dev.js deleted file mode 100644 index 3d986ec..0000000 --- a/exercises/01.exercises/05.problem.bootstrap/dev.js +++ /dev/null @@ -1,42 +0,0 @@ -import { spawn } from 'node:child_process' -import chalk from 'chalk' -import closeWithGrace from 'close-with-grace' -import getPort 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 ssrServer = spawnScript( - 'node', - ['--watch', 'server/ssr.js'], - { PORT: SSR_PORT }, - chalk.blue.bgBlack('SSR'), -) - -closeWithGrace(async () => { - console.log('Shutting down servers...') - const ssrExit = new Promise(resolve => ssrServer.on('exit', resolve)) - - ssrServer.kill('SIGTERM') - - await Promise.all([ssrExit]) -}) diff --git a/exercises/01.exercises/05.problem.bootstrap/package-lock.json b/exercises/01.exercises/05.problem.bootstrap/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/05.problem.bootstrap/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/05.problem.bootstrap/package.json b/exercises/01.exercises/05.problem.bootstrap/package.json deleted file mode 100644 index 88cd509..0000000 --- a/exercises/01.exercises/05.problem.bootstrap/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__05.problem.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/01.exercises/05.problem.bootstrap/public/favicon.ico b/exercises/01.exercises/05.problem.bootstrap/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/05.problem.bootstrap/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/05.problem.bootstrap/public/favicon.svg b/exercises/01.exercises/05.problem.bootstrap/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/05.problem.bootstrap/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/05.problem.bootstrap/server/ssr.js b/exercises/01.exercises/05.problem.bootstrap/server/ssr.js deleted file mode 100644 index d12e9a5..0000000 --- a/exercises/01.exercises/05.problem.bootstrap/server/ssr.js +++ /dev/null @@ -1,58 +0,0 @@ -import closeWithGrace from 'close-with-grace' -import compress from 'compression' -import express from 'express' -import { createElement as h } from 'react' -import { renderToPipeableStream } from 'react-dom/server' -import { Document } from '../src/app.js' -import { shipDataStorage } from './async-storage.js' - -const PORT = process.env.PORT || 3000 - -const app = express() - -app.use(compress()) - -app.head('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) -// ๐Ÿจ add a middleware to serve our js src files at /js/src -// ๐Ÿ’ฐ this isn't an express workshop, so here you go: -// app.use('/js/src', express.static('src')) - -app.get('/:shipId?', async function (req, res) { - try { - const shipId = req.params.shipId || null - const search = req.query.search || '' - res.set('Content-type', 'text/html') - shipDataStorage.run({ shipId, search }, () => { - const root = h(Document) - const { pipe } = renderToPipeableStream( - root, - // ๐Ÿจ add an object here for options - // ๐Ÿจ add the option bootstrapModules that's an array with the string - // '/js/src/index.js' to load our src/index.js file into the browser - ) - pipe(res) - }) - } catch (e) { - console.error(`Failed to SSR: ${e.stack}`) - res.statusCode = 500 - res.end(`Failed to SSR: ${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/01.exercises/05.problem.bootstrap/src/app.js b/exercises/01.exercises/05.problem.bootstrap/src/app.js deleted file mode 100644 index 90cbc73..0000000 --- a/exercises/01.exercises/05.problem.bootstrap/src/app.js +++ /dev/null @@ -1,67 +0,0 @@ -import { Fragment, createElement as h, Suspense } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { ShipDetails, ShipFallback } from './ship-details.js' -import { SearchResults, SearchResultsFallback } from './ship-search-results.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))), - ) -} - -function App() { - const { shipId, search } = shipDataStorage.getStore() - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h( - 'form', - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - name: 'search', - defaultValue: search, - autoFocus: true, - }), - ), - h( - 'ul', - null, - h(Suspense, { fallback: h(SearchResultsFallback) }, h(SearchResults)), - ), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? h(Suspense, { fallback: h(ShipFallback) }, h(ShipDetails)) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/05.problem.bootstrap/src/ship-details.js b/exercises/01.exercises/05.problem.bootstrap/src/ship-details.js deleted file mode 100644 index a8b4b30..0000000 --- a/exercises/01.exercises/05.problem.bootstrap/src/ship-details.js +++ /dev/null @@ -1,84 +0,0 @@ -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' - -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('img', { 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('img', { - 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)')), - ), - ), - ), - ), - ) -} diff --git a/exercises/01.exercises/05.problem.bootstrap/src/ship-search-results.js b/exercises/01.exercises/05.problem.bootstrap/src/ship-search-results.js deleted file mode 100644 index b686499..0000000 --- a/exercises/01.exercises/05.problem.bootstrap/src/ship-search-results.js +++ /dev/null @@ -1,50 +0,0 @@ -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' - -export async function SearchResults() { - const { shipId: currentShipId, search } = shipDataStorage.getStore() - const shipResults = await searchShips({ search }) - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - 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/01.exercises/05.solution.bootstrap/.prettierignore b/exercises/01.exercises/05.solution.bootstrap/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/05.solution.bootstrap/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/05.solution.bootstrap/.prettierrc b/exercises/01.exercises/05.solution.bootstrap/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/05.solution.bootstrap/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/05.solution.bootstrap/README.mdx b/exercises/01.exercises/05.solution.bootstrap/README.mdx deleted file mode 100644 index 14600f4..0000000 --- a/exercises/01.exercises/05.solution.bootstrap/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# Bootstrap Modules diff --git a/exercises/01.exercises/05.solution.bootstrap/db/ship-api.js b/exercises/01.exercises/05.solution.bootstrap/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/05.solution.bootstrap/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/05.solution.bootstrap/db/ships.json b/exercises/01.exercises/05.solution.bootstrap/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/05.solution.bootstrap/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/05.solution.bootstrap/dev.js b/exercises/01.exercises/05.solution.bootstrap/dev.js deleted file mode 100644 index 3d986ec..0000000 --- a/exercises/01.exercises/05.solution.bootstrap/dev.js +++ /dev/null @@ -1,42 +0,0 @@ -import { spawn } from 'node:child_process' -import chalk from 'chalk' -import closeWithGrace from 'close-with-grace' -import getPort 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 ssrServer = spawnScript( - 'node', - ['--watch', 'server/ssr.js'], - { PORT: SSR_PORT }, - chalk.blue.bgBlack('SSR'), -) - -closeWithGrace(async () => { - console.log('Shutting down servers...') - const ssrExit = new Promise(resolve => ssrServer.on('exit', resolve)) - - ssrServer.kill('SIGTERM') - - await Promise.all([ssrExit]) -}) diff --git a/exercises/01.exercises/05.solution.bootstrap/package-lock.json b/exercises/01.exercises/05.solution.bootstrap/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/05.solution.bootstrap/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/05.solution.bootstrap/package.json b/exercises/01.exercises/05.solution.bootstrap/package.json deleted file mode 100644 index 2261900..0000000 --- a/exercises/01.exercises/05.solution.bootstrap/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__05.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/01.exercises/05.solution.bootstrap/public/favicon.ico b/exercises/01.exercises/05.solution.bootstrap/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/05.solution.bootstrap/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/05.solution.bootstrap/public/favicon.svg b/exercises/01.exercises/05.solution.bootstrap/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/05.solution.bootstrap/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/05.solution.bootstrap/server/ssr.js b/exercises/01.exercises/05.solution.bootstrap/server/ssr.js deleted file mode 100644 index f11a398..0000000 --- a/exercises/01.exercises/05.solution.bootstrap/server/ssr.js +++ /dev/null @@ -1,53 +0,0 @@ -import closeWithGrace from 'close-with-grace' -import compress from 'compression' -import express from 'express' -import { createElement as h } from 'react' -import { renderToPipeableStream } from 'react-dom/server' -import { Document } from '../src/app.js' -import { shipDataStorage } from './async-storage.js' - -const PORT = process.env.PORT || 3000 - -const app = express() - -app.use(compress()) - -app.head('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) -app.use('/js/src', express.static('src')) - -app.get('/:shipId?', async function (req, res) { - try { - const shipId = req.params.shipId || null - const search = req.query.search || '' - res.set('Content-type', 'text/html') - shipDataStorage.run({ shipId, search }, () => { - const root = h(Document) - const { pipe } = renderToPipeableStream(root, { - bootstrapModules: ['/js/src/index.js'], - }) - pipe(res) - }) - } catch (e) { - console.error(`Failed to SSR: ${e.stack}`) - res.statusCode = 500 - res.end(`Failed to SSR: ${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/01.exercises/05.solution.bootstrap/src/app.js b/exercises/01.exercises/05.solution.bootstrap/src/app.js deleted file mode 100644 index 90cbc73..0000000 --- a/exercises/01.exercises/05.solution.bootstrap/src/app.js +++ /dev/null @@ -1,67 +0,0 @@ -import { Fragment, createElement as h, Suspense } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { ShipDetails, ShipFallback } from './ship-details.js' -import { SearchResults, SearchResultsFallback } from './ship-search-results.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))), - ) -} - -function App() { - const { shipId, search } = shipDataStorage.getStore() - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h( - 'form', - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - name: 'search', - defaultValue: search, - autoFocus: true, - }), - ), - h( - 'ul', - null, - h(Suspense, { fallback: h(SearchResultsFallback) }, h(SearchResults)), - ), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? h(Suspense, { fallback: h(ShipFallback) }, h(ShipDetails)) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/05.solution.bootstrap/src/index.js b/exercises/01.exercises/05.solution.bootstrap/src/index.js deleted file mode 100644 index 6697cef..0000000 --- a/exercises/01.exercises/05.solution.bootstrap/src/index.js +++ /dev/null @@ -1 +0,0 @@ -console.log('bootstrap the app here!') diff --git a/exercises/01.exercises/05.solution.bootstrap/src/ship-details.js b/exercises/01.exercises/05.solution.bootstrap/src/ship-details.js deleted file mode 100644 index a8b4b30..0000000 --- a/exercises/01.exercises/05.solution.bootstrap/src/ship-details.js +++ /dev/null @@ -1,84 +0,0 @@ -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' - -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('img', { 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('img', { - 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)')), - ), - ), - ), - ), - ) -} diff --git a/exercises/01.exercises/05.solution.bootstrap/src/ship-search-results.js b/exercises/01.exercises/05.solution.bootstrap/src/ship-search-results.js deleted file mode 100644 index b686499..0000000 --- a/exercises/01.exercises/05.solution.bootstrap/src/ship-search-results.js +++ /dev/null @@ -1,50 +0,0 @@ -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' - -export async function SearchResults() { - const { shipId: currentShipId, search } = shipDataStorage.getStore() - const shipResults = await searchShips({ search }) - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - 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/01.exercises/06.problem.import-map/.prettierignore b/exercises/01.exercises/06.problem.import-map/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/06.problem.import-map/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/06.problem.import-map/.prettierrc b/exercises/01.exercises/06.problem.import-map/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/06.problem.import-map/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/06.problem.import-map/README.mdx b/exercises/01.exercises/06.problem.import-map/README.mdx deleted file mode 100644 index 3c81767..0000000 --- a/exercises/01.exercises/06.problem.import-map/README.mdx +++ /dev/null @@ -1,11 +0,0 @@ -# Import Map - -Problem: Map imports to the resources they should come from so we don't have to -pass the absolute URL to the resource. - -๐Ÿ’ฐ I'm going to be especially helpful in this one because there's some config -and node/express-specific stuff going on in here that you're not really here to -learn. It's enough for you to know that this is an important part of getting -the right resources to the right place. But spending time futzing around with -the specifics isn't helpful. Feel free to delete my code if you want to try and -do it yourself though. diff --git a/exercises/01.exercises/06.problem.import-map/db/ship-api.js b/exercises/01.exercises/06.problem.import-map/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/06.problem.import-map/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/06.problem.import-map/db/ships.json b/exercises/01.exercises/06.problem.import-map/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/06.problem.import-map/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/06.problem.import-map/dev.js b/exercises/01.exercises/06.problem.import-map/dev.js deleted file mode 100644 index 3d986ec..0000000 --- a/exercises/01.exercises/06.problem.import-map/dev.js +++ /dev/null @@ -1,42 +0,0 @@ -import { spawn } from 'node:child_process' -import chalk from 'chalk' -import closeWithGrace from 'close-with-grace' -import getPort 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 ssrServer = spawnScript( - 'node', - ['--watch', 'server/ssr.js'], - { PORT: SSR_PORT }, - chalk.blue.bgBlack('SSR'), -) - -closeWithGrace(async () => { - console.log('Shutting down servers...') - const ssrExit = new Promise(resolve => ssrServer.on('exit', resolve)) - - ssrServer.kill('SIGTERM') - - await Promise.all([ssrExit]) -}) diff --git a/exercises/01.exercises/06.problem.import-map/package-lock.json b/exercises/01.exercises/06.problem.import-map/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/06.problem.import-map/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/06.problem.import-map/package.json b/exercises/01.exercises/06.problem.import-map/package.json deleted file mode 100644 index 2b29703..0000000 --- a/exercises/01.exercises/06.problem.import-map/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__06.problem.import-map", - "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/01.exercises/06.problem.import-map/public/favicon.ico b/exercises/01.exercises/06.problem.import-map/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/06.problem.import-map/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/06.problem.import-map/public/favicon.svg b/exercises/01.exercises/06.problem.import-map/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/06.problem.import-map/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/06.problem.import-map/server/ssr.js b/exercises/01.exercises/06.problem.import-map/server/ssr.js deleted file mode 100644 index 2b3a0d5..0000000 --- a/exercises/01.exercises/06.problem.import-map/server/ssr.js +++ /dev/null @@ -1,92 +0,0 @@ -// ๐Ÿ’ฐ you'll need these -// 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 } from 'react' -import { renderToPipeableStream } from 'react-dom/server' -import { Document } from '../src/app.js' -import { shipDataStorage } from './async-storage.js' - -const PORT = process.env.PORT || 3000 - -const app = express() - -app.use(compress()) - -app.head('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) -app.use('/js/src', express.static('src')) - -// ๐Ÿจ add a middleware for serving the react-server-dom-esm/client module -// we have to server this file from our own server so dynamic imports are -// relative to our own server (this module is what loads client-side modules!) -// ๐Ÿ’ฐ this isn't a node/express workshop, so I've just written it for you: -// 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.get('/:shipId?', async function (req, res) { - try { - const shipId = req.params.shipId || null - const search = req.query.search || '' - res.set('Content-type', 'text/html') - shipDataStorage.run({ shipId, search }, () => { - const root = h(Document) - const { pipe } = renderToPipeableStream(root, { - bootstrapModules: ['/js/src/index.js'], - // ๐Ÿจ add an importMap object here with imports for: - // react, react-dom, react-error-boundary, and react-server-dom-esm/client - // ๐Ÿฆ‰ It's enough for you to just know that you need to have a way to - // load these modules in the browser. You don't need to learn how to - // configure these URLs specifically. In a real world framework, you'd - // have a bundler that generates a manifest for you. - // ๐Ÿ’ฐ delete this if you really want to try and figure this out yourself - // otherwise, simply uncomment it: - // 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}`) - } -}) - -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/01.exercises/06.problem.import-map/src/app.js b/exercises/01.exercises/06.problem.import-map/src/app.js deleted file mode 100644 index 90cbc73..0000000 --- a/exercises/01.exercises/06.problem.import-map/src/app.js +++ /dev/null @@ -1,67 +0,0 @@ -import { Fragment, createElement as h, Suspense } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { ShipDetails, ShipFallback } from './ship-details.js' -import { SearchResults, SearchResultsFallback } from './ship-search-results.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))), - ) -} - -function App() { - const { shipId, search } = shipDataStorage.getStore() - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h( - 'form', - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - name: 'search', - defaultValue: search, - autoFocus: true, - }), - ), - h( - 'ul', - null, - h(Suspense, { fallback: h(SearchResultsFallback) }, h(SearchResults)), - ), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? h(Suspense, { fallback: h(ShipFallback) }, h(ShipDetails)) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/06.problem.import-map/src/index.js b/exercises/01.exercises/06.problem.import-map/src/index.js deleted file mode 100644 index d066178..0000000 --- a/exercises/01.exercises/06.problem.import-map/src/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// ๐Ÿจ make sure you can import react, react-dom/client, and react-server-dom-esm/client -// by importing them and logging them to the console. -console.log('bootstrap the app here!') diff --git a/exercises/01.exercises/06.problem.import-map/src/ship-details.js b/exercises/01.exercises/06.problem.import-map/src/ship-details.js deleted file mode 100644 index a8b4b30..0000000 --- a/exercises/01.exercises/06.problem.import-map/src/ship-details.js +++ /dev/null @@ -1,84 +0,0 @@ -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' - -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('img', { 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('img', { - 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)')), - ), - ), - ), - ), - ) -} diff --git a/exercises/01.exercises/06.problem.import-map/src/ship-search-results.js b/exercises/01.exercises/06.problem.import-map/src/ship-search-results.js deleted file mode 100644 index b686499..0000000 --- a/exercises/01.exercises/06.problem.import-map/src/ship-search-results.js +++ /dev/null @@ -1,50 +0,0 @@ -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' - -export async function SearchResults() { - const { shipId: currentShipId, search } = shipDataStorage.getStore() - const shipResults = await searchShips({ search }) - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - 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/01.exercises/06.solution.import-map/.prettierignore b/exercises/01.exercises/06.solution.import-map/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/06.solution.import-map/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/06.solution.import-map/.prettierrc b/exercises/01.exercises/06.solution.import-map/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/06.solution.import-map/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/06.solution.import-map/README.mdx b/exercises/01.exercises/06.solution.import-map/README.mdx deleted file mode 100644 index 61e9616..0000000 --- a/exercises/01.exercises/06.solution.import-map/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# Import Map diff --git a/exercises/01.exercises/06.solution.import-map/db/ship-api.js b/exercises/01.exercises/06.solution.import-map/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/06.solution.import-map/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/06.solution.import-map/db/ships.json b/exercises/01.exercises/06.solution.import-map/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/06.solution.import-map/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/06.solution.import-map/dev.js b/exercises/01.exercises/06.solution.import-map/dev.js deleted file mode 100644 index 3d986ec..0000000 --- a/exercises/01.exercises/06.solution.import-map/dev.js +++ /dev/null @@ -1,42 +0,0 @@ -import { spawn } from 'node:child_process' -import chalk from 'chalk' -import closeWithGrace from 'close-with-grace' -import getPort 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 ssrServer = spawnScript( - 'node', - ['--watch', 'server/ssr.js'], - { PORT: SSR_PORT }, - chalk.blue.bgBlack('SSR'), -) - -closeWithGrace(async () => { - console.log('Shutting down servers...') - const ssrExit = new Promise(resolve => ssrServer.on('exit', resolve)) - - ssrServer.kill('SIGTERM') - - await Promise.all([ssrExit]) -}) diff --git a/exercises/01.exercises/06.solution.import-map/package-lock.json b/exercises/01.exercises/06.solution.import-map/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/06.solution.import-map/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/06.solution.import-map/package.json b/exercises/01.exercises/06.solution.import-map/package.json deleted file mode 100644 index 9ce53db..0000000 --- a/exercises/01.exercises/06.solution.import-map/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__06.solution.import-map", - "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/01.exercises/06.solution.import-map/public/favicon.ico b/exercises/01.exercises/06.solution.import-map/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/06.solution.import-map/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/06.solution.import-map/public/favicon.svg b/exercises/01.exercises/06.solution.import-map/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/06.solution.import-map/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/06.solution.import-map/server/ssr.js b/exercises/01.exercises/06.solution.import-map/server/ssr.js deleted file mode 100644 index 04ca6ed..0000000 --- a/exercises/01.exercises/06.solution.import-map/server/ssr.js +++ /dev/null @@ -1,81 +0,0 @@ -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 } from 'react' -import { renderToPipeableStream } from 'react-dom/server' -import { Document } from '../src/app.js' -import { shipDataStorage } from './async-storage.js' - -const PORT = process.env.PORT || 3000 - -const app = express() - -app.use(compress()) - -app.head('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) -app.use('/js/src', express.static('src')) - -// we have to server this file from our own server so dynamic imports are -// relative to our own server (this module is what loads client-side modules!) -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.get('/:shipId?', async function (req, res) { - try { - const shipId = req.params.shipId || null - const search = req.query.search || '' - res.set('Content-type', 'text/html') - shipDataStorage.run({ shipId, search }, () => { - const root = h(Document) - const { pipe } = renderToPipeableStream(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}`) - } -}) - -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/01.exercises/06.solution.import-map/src/app.js b/exercises/01.exercises/06.solution.import-map/src/app.js deleted file mode 100644 index 90cbc73..0000000 --- a/exercises/01.exercises/06.solution.import-map/src/app.js +++ /dev/null @@ -1,67 +0,0 @@ -import { Fragment, createElement as h, Suspense } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { ShipDetails, ShipFallback } from './ship-details.js' -import { SearchResults, SearchResultsFallback } from './ship-search-results.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))), - ) -} - -function App() { - const { shipId, search } = shipDataStorage.getStore() - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h( - 'form', - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - name: 'search', - defaultValue: search, - autoFocus: true, - }), - ), - h( - 'ul', - null, - h(Suspense, { fallback: h(SearchResultsFallback) }, h(SearchResults)), - ), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? h(Suspense, { fallback: h(ShipFallback) }, h(ShipDetails)) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/06.solution.import-map/src/index.js b/exercises/01.exercises/06.solution.import-map/src/index.js deleted file mode 100644 index ede3cf0..0000000 --- a/exercises/01.exercises/06.solution.import-map/src/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { createElement as h } from 'react' -import { hydrateRoot } from 'react-dom/client' -import * as RSC from 'react-server-dom-esm/client' - -console.log({ h, hydrateRoot, RSC }) diff --git a/exercises/01.exercises/06.solution.import-map/src/ship-details.js b/exercises/01.exercises/06.solution.import-map/src/ship-details.js deleted file mode 100644 index a8b4b30..0000000 --- a/exercises/01.exercises/06.solution.import-map/src/ship-details.js +++ /dev/null @@ -1,84 +0,0 @@ -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' - -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('img', { 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('img', { - 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)')), - ), - ), - ), - ), - ) -} diff --git a/exercises/01.exercises/06.solution.import-map/src/ship-search-results.js b/exercises/01.exercises/06.solution.import-map/src/ship-search-results.js deleted file mode 100644 index b686499..0000000 --- a/exercises/01.exercises/06.solution.import-map/src/ship-search-results.js +++ /dev/null @@ -1,50 +0,0 @@ -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' - -export async function SearchResults() { - const { shipId: currentShipId, search } = shipDataStorage.getStore() - const shipResults = await searchShips({ search }) - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - 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/01.exercises/07.problem.module-graph/.prettierignore b/exercises/01.exercises/07.problem.module-graph/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/07.problem.module-graph/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/07.problem.module-graph/.prettierrc b/exercises/01.exercises/07.problem.module-graph/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/07.problem.module-graph/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/07.problem.module-graph/README.mdx b/exercises/01.exercises/07.problem.module-graph/README.mdx deleted file mode 100644 index 7b5a7f6..0000000 --- a/exercises/01.exercises/07.problem.module-graph/README.mdx +++ /dev/null @@ -1,11 +0,0 @@ -# Split Module Graph - -Problem: Support client-side hydration without sending the entire server-side -module code to the client. This enables server-only code which can access the -database, filesystem, etc. without being sent to the client. - - - NOTE: Now that we have the RSC module graph with `react-server-dom-esm` - rendering our app, be sure to take a look at the output by opening the RSC - server directly in the browser and check out the serialized JSX! - diff --git a/exercises/01.exercises/07.problem.module-graph/db/ship-api.js b/exercises/01.exercises/07.problem.module-graph/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/07.problem.module-graph/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/07.problem.module-graph/db/ships.json b/exercises/01.exercises/07.problem.module-graph/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/07.problem.module-graph/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/07.problem.module-graph/dev.js b/exercises/01.exercises/07.problem.module-graph/dev.js deleted file mode 100644 index 3d986ec..0000000 --- a/exercises/01.exercises/07.problem.module-graph/dev.js +++ /dev/null @@ -1,42 +0,0 @@ -import { spawn } from 'node:child_process' -import chalk from 'chalk' -import closeWithGrace from 'close-with-grace' -import getPort 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 ssrServer = spawnScript( - 'node', - ['--watch', 'server/ssr.js'], - { PORT: SSR_PORT }, - chalk.blue.bgBlack('SSR'), -) - -closeWithGrace(async () => { - console.log('Shutting down servers...') - const ssrExit = new Promise(resolve => ssrServer.on('exit', resolve)) - - ssrServer.kill('SIGTERM') - - await Promise.all([ssrExit]) -}) diff --git a/exercises/01.exercises/07.problem.module-graph/package-lock.json b/exercises/01.exercises/07.problem.module-graph/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/07.problem.module-graph/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/07.problem.module-graph/package.json b/exercises/01.exercises/07.problem.module-graph/package.json deleted file mode 100644 index 72054f2..0000000 --- a/exercises/01.exercises/07.problem.module-graph/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__07.problem.module-graph", - "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/01.exercises/07.problem.module-graph/public/favicon.ico b/exercises/01.exercises/07.problem.module-graph/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/07.problem.module-graph/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/07.problem.module-graph/public/favicon.svg b/exercises/01.exercises/07.problem.module-graph/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/07.problem.module-graph/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/07.problem.module-graph/server/ssr.js b/exercises/01.exercises/07.problem.module-graph/server/ssr.js deleted file mode 100644 index 04ca6ed..0000000 --- a/exercises/01.exercises/07.problem.module-graph/server/ssr.js +++ /dev/null @@ -1,81 +0,0 @@ -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 } from 'react' -import { renderToPipeableStream } from 'react-dom/server' -import { Document } from '../src/app.js' -import { shipDataStorage } from './async-storage.js' - -const PORT = process.env.PORT || 3000 - -const app = express() - -app.use(compress()) - -app.head('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) -app.use('/js/src', express.static('src')) - -// we have to server this file from our own server so dynamic imports are -// relative to our own server (this module is what loads client-side modules!) -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.get('/:shipId?', async function (req, res) { - try { - const shipId = req.params.shipId || null - const search = req.query.search || '' - res.set('Content-type', 'text/html') - shipDataStorage.run({ shipId, search }, () => { - const root = h(Document) - const { pipe } = renderToPipeableStream(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}`) - } -}) - -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/01.exercises/07.problem.module-graph/src/app.js b/exercises/01.exercises/07.problem.module-graph/src/app.js deleted file mode 100644 index 90cbc73..0000000 --- a/exercises/01.exercises/07.problem.module-graph/src/app.js +++ /dev/null @@ -1,67 +0,0 @@ -import { Fragment, createElement as h, Suspense } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { ShipDetails, ShipFallback } from './ship-details.js' -import { SearchResults, SearchResultsFallback } from './ship-search-results.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))), - ) -} - -function App() { - const { shipId, search } = shipDataStorage.getStore() - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h( - 'form', - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - name: 'search', - defaultValue: search, - autoFocus: true, - }), - ), - h( - 'ul', - null, - h(Suspense, { fallback: h(SearchResultsFallback) }, h(SearchResults)), - ), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? h(Suspense, { fallback: h(ShipFallback) }, h(ShipDetails)) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/07.problem.module-graph/src/index.js b/exercises/01.exercises/07.problem.module-graph/src/index.js deleted file mode 100644 index ede3cf0..0000000 --- a/exercises/01.exercises/07.problem.module-graph/src/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { createElement as h } from 'react' -import { hydrateRoot } from 'react-dom/client' -import * as RSC from 'react-server-dom-esm/client' - -console.log({ h, hydrateRoot, RSC }) diff --git a/exercises/01.exercises/07.problem.module-graph/src/ship-details.js b/exercises/01.exercises/07.problem.module-graph/src/ship-details.js deleted file mode 100644 index a8b4b30..0000000 --- a/exercises/01.exercises/07.problem.module-graph/src/ship-details.js +++ /dev/null @@ -1,84 +0,0 @@ -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' - -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('img', { 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('img', { - 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)')), - ), - ), - ), - ), - ) -} diff --git a/exercises/01.exercises/07.problem.module-graph/src/ship-search-results.js b/exercises/01.exercises/07.problem.module-graph/src/ship-search-results.js deleted file mode 100644 index b686499..0000000 --- a/exercises/01.exercises/07.problem.module-graph/src/ship-search-results.js +++ /dev/null @@ -1,50 +0,0 @@ -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' - -export async function SearchResults() { - const { shipId: currentShipId, search } = shipDataStorage.getStore() - const shipResults = await searchShips({ search }) - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - 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/01.exercises/07.solution.module-graph/.prettierignore b/exercises/01.exercises/07.solution.module-graph/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/07.solution.module-graph/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/07.solution.module-graph/.prettierrc b/exercises/01.exercises/07.solution.module-graph/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/07.solution.module-graph/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/07.solution.module-graph/README.mdx b/exercises/01.exercises/07.solution.module-graph/README.mdx deleted file mode 100644 index ea72d59..0000000 --- a/exercises/01.exercises/07.solution.module-graph/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# Split Module Graph diff --git a/exercises/01.exercises/07.solution.module-graph/db/ship-api.js b/exercises/01.exercises/07.solution.module-graph/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/07.solution.module-graph/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/07.solution.module-graph/db/ships.json b/exercises/01.exercises/07.solution.module-graph/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/07.solution.module-graph/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/07.solution.module-graph/dev.js b/exercises/01.exercises/07.solution.module-graph/dev.js deleted file mode 100644 index 81780de..0000000 --- a/exercises/01.exercises/07.solution.module-graph/dev.js +++ /dev/null @@ -1,52 +0,0 @@ -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', '--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/01.exercises/07.solution.module-graph/package-lock.json b/exercises/01.exercises/07.solution.module-graph/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/07.solution.module-graph/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/07.solution.module-graph/package.json b/exercises/01.exercises/07.solution.module-graph/package.json deleted file mode 100644 index 1d95125..0000000 --- a/exercises/01.exercises/07.solution.module-graph/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__07.solution.module-graph", - "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/01.exercises/07.solution.module-graph/public/favicon.ico b/exercises/01.exercises/07.solution.module-graph/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/07.solution.module-graph/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/07.solution.module-graph/public/favicon.svg b/exercises/01.exercises/07.solution.module-graph/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/07.solution.module-graph/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/07.solution.module-graph/server/rsc.js b/exercises/01.exercises/07.solution.module-graph/server/rsc.js deleted file mode 100644 index db93f58..0000000 --- a/exercises/01.exercises/07.solution.module-graph/server/rsc.js +++ /dev/null @@ -1,42 +0,0 @@ -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/01.exercises/07.solution.module-graph/server/ssr.js b/exercises/01.exercises/07.solution.module-graph/server/ssr.js deleted file mode 100644 index bf5606f..0000000 --- a/exercises/01.exercises/07.solution.module-graph/server/ssr.js +++ /dev/null @@ -1,116 +0,0 @@ -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' - -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('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) -app.use('/js/src', express.static('src')) - -// we have to server this file from our own server so dynamic imports are -// relative to our own server (this module is what loads client-side modules!) -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.get('/:shipId?', async function (req, res) { - const promiseForData = request( - { - host: RSC_ORIGIN.hostname, - port: RSC_ORIGIN.port, - method: req.method, - path: req.url, - headers: req.headers, - }, - req, - ) - - try { - res.set('Content-type', 'text/html') - const rscResponse = await promiseForData - const moduleBaseURL = '/js/src' - - let contentPromise - function Root() { - contentPromise ??= createFromNodeStream( - rscResponse, - moduleBasePath, - moduleBaseURL, - ) - const content = use(contentPromise) - return content.root - } - const { pipe } = renderToPipeableStream(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}`) - } -}) - -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/01.exercises/07.solution.module-graph/src/app.js b/exercises/01.exercises/07.solution.module-graph/src/app.js deleted file mode 100644 index 90cbc73..0000000 --- a/exercises/01.exercises/07.solution.module-graph/src/app.js +++ /dev/null @@ -1,67 +0,0 @@ -import { Fragment, createElement as h, Suspense } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { ShipDetails, ShipFallback } from './ship-details.js' -import { SearchResults, SearchResultsFallback } from './ship-search-results.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))), - ) -} - -function App() { - const { shipId, search } = shipDataStorage.getStore() - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h( - 'form', - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - name: 'search', - defaultValue: search, - autoFocus: true, - }), - ), - h( - 'ul', - null, - h(Suspense, { fallback: h(SearchResultsFallback) }, h(SearchResults)), - ), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? h(Suspense, { fallback: h(ShipFallback) }, h(ShipDetails)) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/07.solution.module-graph/src/index.js b/exercises/01.exercises/07.solution.module-graph/src/index.js deleted file mode 100644 index ede3cf0..0000000 --- a/exercises/01.exercises/07.solution.module-graph/src/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { createElement as h } from 'react' -import { hydrateRoot } from 'react-dom/client' -import * as RSC from 'react-server-dom-esm/client' - -console.log({ h, hydrateRoot, RSC }) diff --git a/exercises/01.exercises/07.solution.module-graph/src/ship-details.js b/exercises/01.exercises/07.solution.module-graph/src/ship-details.js deleted file mode 100644 index a8b4b30..0000000 --- a/exercises/01.exercises/07.solution.module-graph/src/ship-details.js +++ /dev/null @@ -1,84 +0,0 @@ -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' - -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('img', { 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('img', { - 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)')), - ), - ), - ), - ), - ) -} diff --git a/exercises/01.exercises/07.solution.module-graph/src/ship-search-results.js b/exercises/01.exercises/07.solution.module-graph/src/ship-search-results.js deleted file mode 100644 index b686499..0000000 --- a/exercises/01.exercises/07.solution.module-graph/src/ship-search-results.js +++ /dev/null @@ -1,50 +0,0 @@ -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' - -export async function SearchResults() { - const { shipId: currentShipId, search } = shipDataStorage.getStore() - const shipResults = await searchShips({ search }) - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - 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/01.exercises/08.problem.hydrate/.prettierignore b/exercises/01.exercises/08.problem.hydrate/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/08.problem.hydrate/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/08.problem.hydrate/.prettierrc b/exercises/01.exercises/08.problem.hydrate/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/08.problem.hydrate/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/08.problem.hydrate/README.mdx b/exercises/01.exercises/08.problem.hydrate/README.mdx deleted file mode 100644 index 4292713..0000000 --- a/exercises/01.exercises/08.problem.hydrate/README.mdx +++ /dev/null @@ -1,11 +0,0 @@ -# Hydrate - -Go to and you'll notice we're not handling errors -well. React Error Boundaries require client-side code. - - - Because Error Boundaries are typically what you want to display when an error - occurs, Remix will simulate Error Boundary behavior on the server so you will - server-render the error rather than render the Suspense fallback followed by - the error boundary. - diff --git a/exercises/01.exercises/08.problem.hydrate/db/ship-api.js b/exercises/01.exercises/08.problem.hydrate/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/08.problem.hydrate/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/08.problem.hydrate/db/ships.json b/exercises/01.exercises/08.problem.hydrate/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/08.problem.hydrate/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/08.problem.hydrate/dev.js b/exercises/01.exercises/08.problem.hydrate/dev.js deleted file mode 100644 index 81780de..0000000 --- a/exercises/01.exercises/08.problem.hydrate/dev.js +++ /dev/null @@ -1,52 +0,0 @@ -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', '--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/01.exercises/08.problem.hydrate/package-lock.json b/exercises/01.exercises/08.problem.hydrate/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/08.problem.hydrate/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/08.problem.hydrate/package.json b/exercises/01.exercises/08.problem.hydrate/package.json deleted file mode 100644 index 2460ed8..0000000 --- a/exercises/01.exercises/08.problem.hydrate/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__08.problem.hydrate", - "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/01.exercises/08.problem.hydrate/public/favicon.ico b/exercises/01.exercises/08.problem.hydrate/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/08.problem.hydrate/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/08.problem.hydrate/public/favicon.svg b/exercises/01.exercises/08.problem.hydrate/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/08.problem.hydrate/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/08.problem.hydrate/server/rsc.js b/exercises/01.exercises/08.problem.hydrate/server/rsc.js deleted file mode 100644 index db93f58..0000000 --- a/exercises/01.exercises/08.problem.hydrate/server/rsc.js +++ /dev/null @@ -1,42 +0,0 @@ -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/01.exercises/08.problem.hydrate/server/ssr.js b/exercises/01.exercises/08.problem.hydrate/server/ssr.js deleted file mode 100644 index bf5606f..0000000 --- a/exercises/01.exercises/08.problem.hydrate/server/ssr.js +++ /dev/null @@ -1,116 +0,0 @@ -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' - -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('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) -app.use('/js/src', express.static('src')) - -// we have to server this file from our own server so dynamic imports are -// relative to our own server (this module is what loads client-side modules!) -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.get('/:shipId?', async function (req, res) { - const promiseForData = request( - { - host: RSC_ORIGIN.hostname, - port: RSC_ORIGIN.port, - method: req.method, - path: req.url, - headers: req.headers, - }, - req, - ) - - try { - res.set('Content-type', 'text/html') - const rscResponse = await promiseForData - const moduleBaseURL = '/js/src' - - let contentPromise - function Root() { - contentPromise ??= createFromNodeStream( - rscResponse, - moduleBasePath, - moduleBaseURL, - ) - const content = use(contentPromise) - return content.root - } - const { pipe } = renderToPipeableStream(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}`) - } -}) - -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/01.exercises/08.problem.hydrate/src/app.js b/exercises/01.exercises/08.problem.hydrate/src/app.js deleted file mode 100644 index 90cbc73..0000000 --- a/exercises/01.exercises/08.problem.hydrate/src/app.js +++ /dev/null @@ -1,67 +0,0 @@ -import { Fragment, createElement as h, Suspense } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { ShipDetails, ShipFallback } from './ship-details.js' -import { SearchResults, SearchResultsFallback } from './ship-search-results.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))), - ) -} - -function App() { - const { shipId, search } = shipDataStorage.getStore() - return h( - 'div', - { className: 'app' }, - h( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h( - 'form', - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - name: 'search', - defaultValue: search, - autoFocus: true, - }), - ), - h( - 'ul', - null, - h(Suspense, { fallback: h(SearchResultsFallback) }, h(SearchResults)), - ), - ), - ), - h( - 'div', - { className: 'details' }, - shipId - ? h(Suspense, { fallback: h(ShipFallback) }, h(ShipDetails)) - : h('p', null, 'Select a ship from the list to see details'), - ), - ) -} diff --git a/exercises/01.exercises/08.problem.hydrate/src/error-boundary.js b/exercises/01.exercises/08.problem.hydrate/src/error-boundary.js deleted file mode 100644 index 9a93e62..0000000 --- a/exercises/01.exercises/08.problem.hydrate/src/error-boundary.js +++ /dev/null @@ -1,4 +0,0 @@ -// https://github.com/bvaughn/react-error-boundary/issues/182 -'use client' - -export { ErrorBoundary } from 'react-error-boundary' diff --git a/exercises/01.exercises/08.problem.hydrate/src/index.js b/exercises/01.exercises/08.problem.hydrate/src/index.js deleted file mode 100644 index ede3cf0..0000000 --- a/exercises/01.exercises/08.problem.hydrate/src/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import { createElement as h } from 'react' -import { hydrateRoot } from 'react-dom/client' -import * as RSC from 'react-server-dom-esm/client' - -console.log({ h, hydrateRoot, RSC }) diff --git a/exercises/01.exercises/08.problem.hydrate/src/ship-details.js b/exercises/01.exercises/08.problem.hydrate/src/ship-details.js deleted file mode 100644 index a8b4b30..0000000 --- a/exercises/01.exercises/08.problem.hydrate/src/ship-details.js +++ /dev/null @@ -1,84 +0,0 @@ -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' - -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('img', { 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('img', { - 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)')), - ), - ), - ), - ), - ) -} diff --git a/exercises/01.exercises/08.problem.hydrate/src/ship-search-results.js b/exercises/01.exercises/08.problem.hydrate/src/ship-search-results.js deleted file mode 100644 index b686499..0000000 --- a/exercises/01.exercises/08.problem.hydrate/src/ship-search-results.js +++ /dev/null @@ -1,50 +0,0 @@ -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' - -export async function SearchResults() { - const { shipId: currentShipId, search } = shipDataStorage.getStore() - const shipResults = await searchShips({ search }) - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - 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/01.exercises/08.solution.hydrate/.prettierignore b/exercises/01.exercises/08.solution.hydrate/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/08.solution.hydrate/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/08.solution.hydrate/.prettierrc b/exercises/01.exercises/08.solution.hydrate/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/08.solution.hydrate/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/08.solution.hydrate/README.mdx b/exercises/01.exercises/08.solution.hydrate/README.mdx deleted file mode 100644 index c3bd872..0000000 --- a/exercises/01.exercises/08.solution.hydrate/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# Hydrate diff --git a/exercises/01.exercises/08.solution.hydrate/db/ship-api.js b/exercises/01.exercises/08.solution.hydrate/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/08.solution.hydrate/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/08.solution.hydrate/db/ships.json b/exercises/01.exercises/08.solution.hydrate/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/08.solution.hydrate/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/08.solution.hydrate/dev.js b/exercises/01.exercises/08.solution.hydrate/dev.js deleted file mode 100644 index b10826e..0000000 --- a/exercises/01.exercises/08.solution.hydrate/dev.js +++ /dev/null @@ -1,58 +0,0 @@ -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/01.exercises/08.solution.hydrate/package-lock.json b/exercises/01.exercises/08.solution.hydrate/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/08.solution.hydrate/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/08.solution.hydrate/package.json b/exercises/01.exercises/08.solution.hydrate/package.json deleted file mode 100644 index e5625ee..0000000 --- a/exercises/01.exercises/08.solution.hydrate/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__08.solution.hydrate", - "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/01.exercises/08.solution.hydrate/public/favicon.ico b/exercises/01.exercises/08.solution.hydrate/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/08.solution.hydrate/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/08.solution.hydrate/public/favicon.svg b/exercises/01.exercises/08.solution.hydrate/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/08.solution.hydrate/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/08.solution.hydrate/server/rsc-loader.js b/exercises/01.exercises/08.solution.hydrate/server/rsc-loader.js deleted file mode 100644 index 836ca6f..0000000 --- a/exercises/01.exercises/08.solution.hydrate/server/rsc-loader.js +++ /dev/null @@ -1,23 +0,0 @@ -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/01.exercises/08.solution.hydrate/server/rsc.js b/exercises/01.exercises/08.solution.hydrate/server/rsc.js deleted file mode 100644 index db93f58..0000000 --- a/exercises/01.exercises/08.solution.hydrate/server/rsc.js +++ /dev/null @@ -1,42 +0,0 @@ -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/01.exercises/08.solution.hydrate/server/ssr.js b/exercises/01.exercises/08.solution.hydrate/server/ssr.js deleted file mode 100644 index 100ee25..0000000 --- a/exercises/01.exercises/08.solution.hydrate/server/ssr.js +++ /dev/null @@ -1,141 +0,0 @@ -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' - -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('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) -app.use('/js/src', express.static('src')) - -// we have to server this file from our own server so dynamic imports are -// relative to our own server (this module is what loads client-side modules!) -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) { - const promiseForData = request( - { - host: RSC_ORIGIN.hostname, - port: RSC_ORIGIN.port, - method: req.method, - path: req.url, - headers: req.headers, - }, - req, - ) - - if (req.accepts('text/html')) { - try { - res.set('Content-type', 'text/html') - const rscResponse = await promiseForData - const moduleBaseURL = '/js/src' - - let contentPromise - function Root() { - contentPromise ??= createFromNodeStream( - rscResponse, - moduleBasePath, - moduleBaseURL, - ) - const content = use(contentPromise) - return content.root - } - const { pipe } = renderToPipeableStream(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) - }) - - 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/01.exercises/08.solution.hydrate/src/app.js b/exercises/01.exercises/08.solution.hydrate/src/app.js deleted file mode 100644 index 3ee5914..0000000 --- a/exercises/01.exercises/08.solution.hydrate/src/app.js +++ /dev/null @@ -1,86 +0,0 @@ -import { Fragment, createElement as h, Suspense } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { ErrorBoundary } from './error-boundary.js' -import { ShipDetails, ShipError, ShipFallback } from './ship-details.js' -import { SearchResults, SearchResultsFallback } from './ship-search-results.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))), - ) -} - -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( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h( - 'form', - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - name: 'search', - defaultValue: search, - autoFocus: true, - }), - ), - h( - 'ul', - null, - h( - Suspense, - { fallback: h(SearchResultsFallback) }, - h(SearchResults), - ), - ), - ), - ), - h( - 'div', - { className: 'details' }, - 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/01.exercises/08.solution.hydrate/src/error-boundary.js b/exercises/01.exercises/08.solution.hydrate/src/error-boundary.js deleted file mode 100644 index 9a93e62..0000000 --- a/exercises/01.exercises/08.solution.hydrate/src/error-boundary.js +++ /dev/null @@ -1,4 +0,0 @@ -// https://github.com/bvaughn/react-error-boundary/issues/182 -'use client' - -export { ErrorBoundary } from 'react-error-boundary' diff --git a/exercises/01.exercises/08.solution.hydrate/src/index.js b/exercises/01.exercises/08.solution.hydrate/src/index.js deleted file mode 100644 index 9e5cc1d..0000000 --- a/exercises/01.exercises/08.solution.hydrate/src/index.js +++ /dev/null @@ -1,30 +0,0 @@ -// We don't need 'use client' on this file because the RSC server never imports -// it anyway. It's only used as a bootstrap module for the client. -// 'use client' - -import { createElement as h, startTransition, use } from 'react' -import { hydrateRoot } from 'react-dom/client' -import * as RSC from 'react-server-dom-esm/client' - -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() { - return use(initialContentPromise).root -} - -startTransition(() => { - hydrateRoot(document, h(Root)) -}) diff --git a/exercises/01.exercises/08.solution.hydrate/src/ship-details.js b/exercises/01.exercises/08.solution.hydrate/src/ship-details.js deleted file mode 100644 index 995bf3c..0000000 --- a/exercises/01.exercises/08.solution.hydrate/src/ship-details.js +++ /dev/null @@ -1,99 +0,0 @@ -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' - -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('img', { 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('img', { - 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/01.exercises/08.solution.hydrate/src/ship-search-results.js b/exercises/01.exercises/08.solution.hydrate/src/ship-search-results.js deleted file mode 100644 index b686499..0000000 --- a/exercises/01.exercises/08.solution.hydrate/src/ship-search-results.js +++ /dev/null @@ -1,50 +0,0 @@ -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' - -export async function SearchResults() { - const { shipId: currentShipId, search } = shipDataStorage.getStore() - const shipResults = await searchShips({ search }) - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - 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/01.exercises/09.problem.routing/.prettierignore b/exercises/01.exercises/09.problem.routing/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/09.problem.routing/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/09.problem.routing/.prettierrc b/exercises/01.exercises/09.problem.routing/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/09.problem.routing/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/09.problem.routing/README.mdx b/exercises/01.exercises/09.problem.routing/README.mdx deleted file mode 100644 index 76c16e7..0000000 --- a/exercises/01.exercises/09.problem.routing/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# Client Side Routing diff --git a/exercises/01.exercises/09.problem.routing/db/ship-api.js b/exercises/01.exercises/09.problem.routing/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/09.problem.routing/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/09.problem.routing/db/ships.json b/exercises/01.exercises/09.problem.routing/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/09.problem.routing/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/09.problem.routing/dev.js b/exercises/01.exercises/09.problem.routing/dev.js deleted file mode 100644 index b10826e..0000000 --- a/exercises/01.exercises/09.problem.routing/dev.js +++ /dev/null @@ -1,58 +0,0 @@ -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/01.exercises/09.problem.routing/package-lock.json b/exercises/01.exercises/09.problem.routing/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/09.problem.routing/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/09.problem.routing/package.json b/exercises/01.exercises/09.problem.routing/package.json deleted file mode 100644 index 0beb64a..0000000 --- a/exercises/01.exercises/09.problem.routing/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__09.problem.routing", - "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/01.exercises/09.problem.routing/public/favicon.ico b/exercises/01.exercises/09.problem.routing/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/09.problem.routing/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/09.problem.routing/public/favicon.svg b/exercises/01.exercises/09.problem.routing/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/09.problem.routing/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/09.problem.routing/server/rsc-loader.js b/exercises/01.exercises/09.problem.routing/server/rsc-loader.js deleted file mode 100644 index 836ca6f..0000000 --- a/exercises/01.exercises/09.problem.routing/server/rsc-loader.js +++ /dev/null @@ -1,23 +0,0 @@ -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/01.exercises/09.problem.routing/server/rsc.js b/exercises/01.exercises/09.problem.routing/server/rsc.js deleted file mode 100644 index db93f58..0000000 --- a/exercises/01.exercises/09.problem.routing/server/rsc.js +++ /dev/null @@ -1,42 +0,0 @@ -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/01.exercises/09.problem.routing/server/ssr.js b/exercises/01.exercises/09.problem.routing/server/ssr.js deleted file mode 100644 index 100ee25..0000000 --- a/exercises/01.exercises/09.problem.routing/server/ssr.js +++ /dev/null @@ -1,141 +0,0 @@ -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' - -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('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) -app.use('/js/src', express.static('src')) - -// we have to server this file from our own server so dynamic imports are -// relative to our own server (this module is what loads client-side modules!) -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) { - const promiseForData = request( - { - host: RSC_ORIGIN.hostname, - port: RSC_ORIGIN.port, - method: req.method, - path: req.url, - headers: req.headers, - }, - req, - ) - - if (req.accepts('text/html')) { - try { - res.set('Content-type', 'text/html') - const rscResponse = await promiseForData - const moduleBaseURL = '/js/src' - - let contentPromise - function Root() { - contentPromise ??= createFromNodeStream( - rscResponse, - moduleBasePath, - moduleBaseURL, - ) - const content = use(contentPromise) - return content.root - } - const { pipe } = renderToPipeableStream(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) - }) - - 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/01.exercises/09.problem.routing/src/app.js b/exercises/01.exercises/09.problem.routing/src/app.js deleted file mode 100644 index 3ee5914..0000000 --- a/exercises/01.exercises/09.problem.routing/src/app.js +++ /dev/null @@ -1,86 +0,0 @@ -import { Fragment, createElement as h, Suspense } from 'react' -import { shipDataStorage } from '../server/async-storage.js' -import { ErrorBoundary } from './error-boundary.js' -import { ShipDetails, ShipError, ShipFallback } from './ship-details.js' -import { SearchResults, SearchResultsFallback } from './ship-search-results.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))), - ) -} - -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( - 'div', - { className: 'search' }, - h( - Fragment, - null, - h( - 'form', - null, - h('input', { - placeholder: 'Filter ships...', - type: 'search', - name: 'search', - defaultValue: search, - autoFocus: true, - }), - ), - h( - 'ul', - null, - h( - Suspense, - { fallback: h(SearchResultsFallback) }, - h(SearchResults), - ), - ), - ), - ), - h( - 'div', - { className: 'details' }, - 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/01.exercises/09.problem.routing/src/error-boundary.js b/exercises/01.exercises/09.problem.routing/src/error-boundary.js deleted file mode 100644 index 9a93e62..0000000 --- a/exercises/01.exercises/09.problem.routing/src/error-boundary.js +++ /dev/null @@ -1,4 +0,0 @@ -// https://github.com/bvaughn/react-error-boundary/issues/182 -'use client' - -export { ErrorBoundary } from 'react-error-boundary' diff --git a/exercises/01.exercises/09.problem.routing/src/index.js b/exercises/01.exercises/09.problem.routing/src/index.js deleted file mode 100644 index 9e5cc1d..0000000 --- a/exercises/01.exercises/09.problem.routing/src/index.js +++ /dev/null @@ -1,30 +0,0 @@ -// We don't need 'use client' on this file because the RSC server never imports -// it anyway. It's only used as a bootstrap module for the client. -// 'use client' - -import { createElement as h, startTransition, use } from 'react' -import { hydrateRoot } from 'react-dom/client' -import * as RSC from 'react-server-dom-esm/client' - -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() { - return use(initialContentPromise).root -} - -startTransition(() => { - hydrateRoot(document, h(Root)) -}) diff --git a/exercises/01.exercises/09.problem.routing/src/ship-details.js b/exercises/01.exercises/09.problem.routing/src/ship-details.js deleted file mode 100644 index 995bf3c..0000000 --- a/exercises/01.exercises/09.problem.routing/src/ship-details.js +++ /dev/null @@ -1,99 +0,0 @@ -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' - -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('img', { 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('img', { - 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/01.exercises/09.problem.routing/src/ship-search-results.js b/exercises/01.exercises/09.problem.routing/src/ship-search-results.js deleted file mode 100644 index b686499..0000000 --- a/exercises/01.exercises/09.problem.routing/src/ship-search-results.js +++ /dev/null @@ -1,50 +0,0 @@ -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' - -export async function SearchResults() { - const { shipId: currentShipId, search } = shipDataStorage.getStore() - const shipResults = await searchShips({ search }) - return shipResults.ships.map(ship => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( - 'li', - { key: ship.name }, - h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { - 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/01.exercises/09.solution.routing/.prettierignore b/exercises/01.exercises/09.solution.routing/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/09.solution.routing/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/09.solution.routing/.prettierrc b/exercises/01.exercises/09.solution.routing/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/09.solution.routing/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/09.solution.routing/README.mdx b/exercises/01.exercises/09.solution.routing/README.mdx deleted file mode 100644 index 76c16e7..0000000 --- a/exercises/01.exercises/09.solution.routing/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# Client Side Routing diff --git a/exercises/01.exercises/09.solution.routing/db/ship-api.js b/exercises/01.exercises/09.solution.routing/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/09.solution.routing/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/09.solution.routing/db/ships.json b/exercises/01.exercises/09.solution.routing/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/09.solution.routing/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/09.solution.routing/dev.js b/exercises/01.exercises/09.solution.routing/dev.js deleted file mode 100644 index b10826e..0000000 --- a/exercises/01.exercises/09.solution.routing/dev.js +++ /dev/null @@ -1,58 +0,0 @@ -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/01.exercises/09.solution.routing/package-lock.json b/exercises/01.exercises/09.solution.routing/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/09.solution.routing/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/09.solution.routing/package.json b/exercises/01.exercises/09.solution.routing/package.json deleted file mode 100644 index e81210d..0000000 --- a/exercises/01.exercises/09.solution.routing/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__09.solution.routing", - "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/01.exercises/09.solution.routing/public/favicon.ico b/exercises/01.exercises/09.solution.routing/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/09.solution.routing/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/09.solution.routing/public/favicon.svg b/exercises/01.exercises/09.solution.routing/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/09.solution.routing/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/09.solution.routing/server/rsc-loader.js b/exercises/01.exercises/09.solution.routing/server/rsc-loader.js deleted file mode 100644 index 836ca6f..0000000 --- a/exercises/01.exercises/09.solution.routing/server/rsc-loader.js +++ /dev/null @@ -1,23 +0,0 @@ -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/01.exercises/09.solution.routing/server/rsc.js b/exercises/01.exercises/09.solution.routing/server/rsc.js deleted file mode 100644 index cdd1d5b..0000000 --- a/exercises/01.exercises/09.solution.routing/server/rsc.js +++ /dev/null @@ -1,43 +0,0 @@ -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 || '' - res.set('x-location', res.req.url) - 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/01.exercises/09.solution.routing/server/ssr.js b/exercises/01.exercises/09.solution.routing/server/ssr.js deleted file mode 100644 index ef8c222..0000000 --- a/exercises/01.exercises/09.solution.routing/server/ssr.js +++ /dev/null @@ -1,156 +0,0 @@ -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('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) -app.use('/js/src', express.static('src')) - -// we have to server this file from our own server so dynamic imports are -// relative to our own server (this module is what loads client-side modules!) -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) { - const promiseForData = request( - { - host: RSC_ORIGIN.hostname, - port: RSC_ORIGIN.port, - method: req.method, - path: req.url, - headers: req.headers, - }, - req, - ) - - if (req.accepts('text/html')) { - try { - res.set('Content-type', 'text/html') - const rscResponse = await promiseForData - const moduleBaseURL = '/js/src' - - 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) - }) - - 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/01.exercises/09.solution.routing/src/app.js b/exercises/01.exercises/09.solution.routing/src/app.js deleted file mode 100644 index 0fae21a..0000000 --- a/exercises/01.exercises/09.solution.routing/src/app.js +++ /dev/null @@ -1,79 +0,0 @@ -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))), - ) -} - -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/01.exercises/09.solution.routing/src/error-boundary.js b/exercises/01.exercises/09.solution.routing/src/error-boundary.js deleted file mode 100644 index 9a93e62..0000000 --- a/exercises/01.exercises/09.solution.routing/src/error-boundary.js +++ /dev/null @@ -1,4 +0,0 @@ -// https://github.com/bvaughn/react-error-boundary/issues/182 -'use client' - -export { ErrorBoundary } from 'react-error-boundary' diff --git a/exercises/01.exercises/09.solution.routing/src/index.js b/exercises/01.exercises/09.solution.routing/src/index.js deleted file mode 100644 index 8cc6206..0000000 --- a/exercises/01.exercises/09.solution.routing/src/index.js +++ /dev/null @@ -1,134 +0,0 @@ -import { - createElement as h, - startTransition, - use, - useDeferredValue, - useEffect, - useReducer, - useRef, - useState, - useTransition, -} from 'react' -import { hydrateRoot } 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' - -function generateKey() { - return Date.now().toString(36) + Math.random().toString(36).slice(2) -} - -const contentCache = new Map() - -function createFromFetch(fetchPromise) { - return RSC.createFromFetch(fetchPromise, { moduleBaseURL }) -} - -const initialLocation = getGlobalLocation() -const initialContentPromise = createFromFetch(fetchContent(initialLocation)) - -let initialContentKey = window.history.state?.key -if (!initialContentKey) { - initialContentKey = generateKey() - window.history.replaceState({ key: initialContentKey }, '') -} -contentCache.set(initialContentKey, initialContentPromise) - -export function Root() { - const [, forceRender] = useReducer(() => Symbol(), Symbol()) - const latestNav = useRef(null) - const [nextLocation, setNextLocation] = useState(getGlobalLocation) - const [contentKey, setContentKey] = useState(initialContentKey) - const [isPending, startTransition] = useTransition() - - function updateContentKey(newContentKey) { - startTransition(() => setContentKey(newContentKey)) - } - - const location = useDeferredValue(nextLocation) - const contentPromise = contentCache.get(contentKey) - - useEffect(() => { - function handlePopState() { - const nextLocation = getGlobalLocation() - setNextLocation(nextLocation) - const historyKey = window.history.state?.key ?? generateKey() - - const thisNav = Symbol(`Nav for ${historyKey}`) - latestNav.current = thisNav - - let nextContentPromise - const fetchPromise = fetchContent(nextLocation) - // create a promise chain that resolves when the stream is completely consumed - fetchPromise - // clone the response so createFromFetch can use it (otherwise we lock the reader) - // and wait for the text to be consumed so we know the stream is finished - .then(response => response.clone().text()) - .then(() => { - contentCache.set(historyKey, nextContentPromise) - if (thisNav === latestNav.current) { - // trigger a rerender now that the updated content is in the cache - startTransition(() => forceRender()) - } - }) - nextContentPromise = createFromFetch(fetchPromise) - - if (!contentCache.has(historyKey)) { - // if we don't have this key in the cache already, set it now - contentCache.set(historyKey, nextContentPromise) - } - - updateContentKey(historyKey) - } - window.addEventListener('popstate', handlePopState) - return () => window.removeEventListener('popstate', handlePopState) - }, []) - - async function navigate(nextLocation, { replace = false, contentKey } = {}) { - setNextLocation(nextLocation) - const thisNav = Symbol() - latestNav.current = thisNav - - const newContentKey = contentKey ?? generateKey() - const nextContentPromise = createFromFetch( - fetchContent(nextLocation).then(response => { - if (thisNav !== latestNav.current) return - const newLocation = response.headers.get('x-location') - if (replace) { - window.history.replaceState({ key: newContentKey }, '', newLocation) - } else { - window.history.pushState({ key: newContentKey }, '', newLocation) - } - return response - }), - ) - - contentCache.set(newContentKey, nextContentPromise) - updateContentKey(newContentKey) - } - - return h( - RouterContext.Provider, - { - value: { - location, - nextLocation: isPending ? nextLocation : location, - navigate, - isPending, - }, - }, - use(contentPromise).root, - ) -} - -startTransition(() => { - hydrateRoot(document, h(Root)) -}) diff --git a/exercises/01.exercises/09.solution.routing/src/router.js b/exercises/01.exercises/09.solution.routing/src/router.js deleted file mode 100644 index 248e678..0000000 --- a/exercises/01.exercises/09.solution.routing/src/router.js +++ /dev/null @@ -1,34 +0,0 @@ -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/01.exercises/09.solution.routing/src/ship-details-pending.js b/exercises/01.exercises/09.solution.routing/src/ship-details-pending.js deleted file mode 100644 index 0d98be6..0000000 --- a/exercises/01.exercises/09.solution.routing/src/ship-details-pending.js +++ /dev/null @@ -1,21 +0,0 @@ -'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/01.exercises/09.solution.routing/src/ship-details.js b/exercises/01.exercises/09.solution.routing/src/ship-details.js deleted file mode 100644 index 1e12822..0000000 --- a/exercises/01.exercises/09.solution.routing/src/ship-details.js +++ /dev/null @@ -1,100 +0,0 @@ -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/01.exercises/09.solution.routing/src/ship-search-results.js b/exercises/01.exercises/09.solution.routing/src/ship-search-results.js deleted file mode 100644 index fb4f956..0000000 --- a/exercises/01.exercises/09.solution.routing/src/ship-search-results.js +++ /dev/null @@ -1,43 +0,0 @@ -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 { SelectShipLink } 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( - SelectShipLink, - { 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/01.exercises/09.solution.routing/src/ship-search.js b/exercises/01.exercises/09.solution.routing/src/ship-search.js deleted file mode 100644 index b7f6899..0000000 --- a/exercises/01.exercises/09.solution.routing/src/ship-search.js +++ /dev/null @@ -1,68 +0,0 @@ -'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 SelectShipLink({ 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/01.exercises/09.solution.routing/src/spin-delay.js b/exercises/01.exercises/09.solution.routing/src/spin-delay.js deleted file mode 100644 index e5fd221..0000000 --- a/exercises/01.exercises/09.solution.routing/src/spin-delay.js +++ /dev/null @@ -1,35 +0,0 @@ -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' -} diff --git a/exercises/01.exercises/10.problem.actions/.prettierignore b/exercises/01.exercises/10.problem.actions/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/10.problem.actions/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/10.problem.actions/.prettierrc b/exercises/01.exercises/10.problem.actions/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/10.problem.actions/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/10.problem.actions/README.mdx b/exercises/01.exercises/10.problem.actions/README.mdx deleted file mode 100644 index 6378f3d..0000000 --- a/exercises/01.exercises/10.problem.actions/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# Server Actions diff --git a/exercises/01.exercises/10.problem.actions/db/ship-api.js b/exercises/01.exercises/10.problem.actions/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/10.problem.actions/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/10.problem.actions/db/ships.json b/exercises/01.exercises/10.problem.actions/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/10.problem.actions/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/10.problem.actions/dev.js b/exercises/01.exercises/10.problem.actions/dev.js deleted file mode 100644 index b10826e..0000000 --- a/exercises/01.exercises/10.problem.actions/dev.js +++ /dev/null @@ -1,58 +0,0 @@ -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/01.exercises/10.problem.actions/package-lock.json b/exercises/01.exercises/10.problem.actions/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/10.problem.actions/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/10.problem.actions/package.json b/exercises/01.exercises/10.problem.actions/package.json deleted file mode 100644 index 2660d84..0000000 --- a/exercises/01.exercises/10.problem.actions/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__10.problem.actions", - "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/01.exercises/10.problem.actions/public/favicon.ico b/exercises/01.exercises/10.problem.actions/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/10.problem.actions/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/10.problem.actions/public/favicon.svg b/exercises/01.exercises/10.problem.actions/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/10.problem.actions/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/10.problem.actions/server/rsc-loader.js b/exercises/01.exercises/10.problem.actions/server/rsc-loader.js deleted file mode 100644 index 836ca6f..0000000 --- a/exercises/01.exercises/10.problem.actions/server/rsc-loader.js +++ /dev/null @@ -1,23 +0,0 @@ -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/01.exercises/10.problem.actions/server/rsc.js b/exercises/01.exercises/10.problem.actions/server/rsc.js deleted file mode 100644 index cdd1d5b..0000000 --- a/exercises/01.exercises/10.problem.actions/server/rsc.js +++ /dev/null @@ -1,43 +0,0 @@ -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 || '' - res.set('x-location', res.req.url) - 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/01.exercises/10.problem.actions/server/ssr.js b/exercises/01.exercises/10.problem.actions/server/ssr.js deleted file mode 100644 index ef8c222..0000000 --- a/exercises/01.exercises/10.problem.actions/server/ssr.js +++ /dev/null @@ -1,156 +0,0 @@ -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('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) -app.use('/js/src', express.static('src')) - -// we have to server this file from our own server so dynamic imports are -// relative to our own server (this module is what loads client-side modules!) -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) { - const promiseForData = request( - { - host: RSC_ORIGIN.hostname, - port: RSC_ORIGIN.port, - method: req.method, - path: req.url, - headers: req.headers, - }, - req, - ) - - if (req.accepts('text/html')) { - try { - res.set('Content-type', 'text/html') - const rscResponse = await promiseForData - const moduleBaseURL = '/js/src' - - 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) - }) - - 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/01.exercises/10.problem.actions/src/app.js b/exercises/01.exercises/10.problem.actions/src/app.js deleted file mode 100644 index 0fae21a..0000000 --- a/exercises/01.exercises/10.problem.actions/src/app.js +++ /dev/null @@ -1,79 +0,0 @@ -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))), - ) -} - -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/01.exercises/10.problem.actions/src/error-boundary.js b/exercises/01.exercises/10.problem.actions/src/error-boundary.js deleted file mode 100644 index 9a93e62..0000000 --- a/exercises/01.exercises/10.problem.actions/src/error-boundary.js +++ /dev/null @@ -1,4 +0,0 @@ -// https://github.com/bvaughn/react-error-boundary/issues/182 -'use client' - -export { ErrorBoundary } from 'react-error-boundary' diff --git a/exercises/01.exercises/10.problem.actions/src/index.js b/exercises/01.exercises/10.problem.actions/src/index.js deleted file mode 100644 index 8cc6206..0000000 --- a/exercises/01.exercises/10.problem.actions/src/index.js +++ /dev/null @@ -1,134 +0,0 @@ -import { - createElement as h, - startTransition, - use, - useDeferredValue, - useEffect, - useReducer, - useRef, - useState, - useTransition, -} from 'react' -import { hydrateRoot } 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' - -function generateKey() { - return Date.now().toString(36) + Math.random().toString(36).slice(2) -} - -const contentCache = new Map() - -function createFromFetch(fetchPromise) { - return RSC.createFromFetch(fetchPromise, { moduleBaseURL }) -} - -const initialLocation = getGlobalLocation() -const initialContentPromise = createFromFetch(fetchContent(initialLocation)) - -let initialContentKey = window.history.state?.key -if (!initialContentKey) { - initialContentKey = generateKey() - window.history.replaceState({ key: initialContentKey }, '') -} -contentCache.set(initialContentKey, initialContentPromise) - -export function Root() { - const [, forceRender] = useReducer(() => Symbol(), Symbol()) - const latestNav = useRef(null) - const [nextLocation, setNextLocation] = useState(getGlobalLocation) - const [contentKey, setContentKey] = useState(initialContentKey) - const [isPending, startTransition] = useTransition() - - function updateContentKey(newContentKey) { - startTransition(() => setContentKey(newContentKey)) - } - - const location = useDeferredValue(nextLocation) - const contentPromise = contentCache.get(contentKey) - - useEffect(() => { - function handlePopState() { - const nextLocation = getGlobalLocation() - setNextLocation(nextLocation) - const historyKey = window.history.state?.key ?? generateKey() - - const thisNav = Symbol(`Nav for ${historyKey}`) - latestNav.current = thisNav - - let nextContentPromise - const fetchPromise = fetchContent(nextLocation) - // create a promise chain that resolves when the stream is completely consumed - fetchPromise - // clone the response so createFromFetch can use it (otherwise we lock the reader) - // and wait for the text to be consumed so we know the stream is finished - .then(response => response.clone().text()) - .then(() => { - contentCache.set(historyKey, nextContentPromise) - if (thisNav === latestNav.current) { - // trigger a rerender now that the updated content is in the cache - startTransition(() => forceRender()) - } - }) - nextContentPromise = createFromFetch(fetchPromise) - - if (!contentCache.has(historyKey)) { - // if we don't have this key in the cache already, set it now - contentCache.set(historyKey, nextContentPromise) - } - - updateContentKey(historyKey) - } - window.addEventListener('popstate', handlePopState) - return () => window.removeEventListener('popstate', handlePopState) - }, []) - - async function navigate(nextLocation, { replace = false, contentKey } = {}) { - setNextLocation(nextLocation) - const thisNav = Symbol() - latestNav.current = thisNav - - const newContentKey = contentKey ?? generateKey() - const nextContentPromise = createFromFetch( - fetchContent(nextLocation).then(response => { - if (thisNav !== latestNav.current) return - const newLocation = response.headers.get('x-location') - if (replace) { - window.history.replaceState({ key: newContentKey }, '', newLocation) - } else { - window.history.pushState({ key: newContentKey }, '', newLocation) - } - return response - }), - ) - - contentCache.set(newContentKey, nextContentPromise) - updateContentKey(newContentKey) - } - - return h( - RouterContext.Provider, - { - value: { - location, - nextLocation: isPending ? nextLocation : location, - navigate, - isPending, - }, - }, - use(contentPromise).root, - ) -} - -startTransition(() => { - hydrateRoot(document, h(Root)) -}) diff --git a/exercises/01.exercises/10.problem.actions/src/router.js b/exercises/01.exercises/10.problem.actions/src/router.js deleted file mode 100644 index 248e678..0000000 --- a/exercises/01.exercises/10.problem.actions/src/router.js +++ /dev/null @@ -1,34 +0,0 @@ -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/01.exercises/10.problem.actions/src/ship-details-pending.js b/exercises/01.exercises/10.problem.actions/src/ship-details-pending.js deleted file mode 100644 index 0d98be6..0000000 --- a/exercises/01.exercises/10.problem.actions/src/ship-details-pending.js +++ /dev/null @@ -1,21 +0,0 @@ -'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/01.exercises/10.problem.actions/src/ship-details.js b/exercises/01.exercises/10.problem.actions/src/ship-details.js deleted file mode 100644 index 1e12822..0000000 --- a/exercises/01.exercises/10.problem.actions/src/ship-details.js +++ /dev/null @@ -1,100 +0,0 @@ -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/01.exercises/10.problem.actions/src/ship-search-results.js b/exercises/01.exercises/10.problem.actions/src/ship-search-results.js deleted file mode 100644 index fb4f956..0000000 --- a/exercises/01.exercises/10.problem.actions/src/ship-search-results.js +++ /dev/null @@ -1,43 +0,0 @@ -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 { SelectShipLink } 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( - SelectShipLink, - { 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/01.exercises/10.problem.actions/src/ship-search.js b/exercises/01.exercises/10.problem.actions/src/ship-search.js deleted file mode 100644 index b7f6899..0000000 --- a/exercises/01.exercises/10.problem.actions/src/ship-search.js +++ /dev/null @@ -1,68 +0,0 @@ -'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 SelectShipLink({ 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/01.exercises/10.problem.actions/src/spin-delay.js b/exercises/01.exercises/10.problem.actions/src/spin-delay.js deleted file mode 100644 index e5fd221..0000000 --- a/exercises/01.exercises/10.problem.actions/src/spin-delay.js +++ /dev/null @@ -1,35 +0,0 @@ -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' -} diff --git a/exercises/01.exercises/10.solution.actions/.prettierignore b/exercises/01.exercises/10.solution.actions/.prettierignore deleted file mode 100644 index 4dd9aa1..0000000 --- a/exercises/01.exercises/10.solution.actions/.prettierignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -package-lock.json -built_node_modules diff --git a/exercises/01.exercises/10.solution.actions/.prettierrc b/exercises/01.exercises/10.solution.actions/.prettierrc deleted file mode 100644 index fc39ebe..0000000 --- a/exercises/01.exercises/10.solution.actions/.prettierrc +++ /dev/null @@ -1,35 +0,0 @@ -{ - "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 - } - }, - { - "files": ["**/*.mdx"], - "options": { - "proseWrap": "preserve", - "htmlWhitespaceSensitivity": "ignore" - } - } - ] -} diff --git a/exercises/01.exercises/10.solution.actions/README.mdx b/exercises/01.exercises/10.solution.actions/README.mdx deleted file mode 100644 index 6378f3d..0000000 --- a/exercises/01.exercises/10.solution.actions/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# Server Actions diff --git a/exercises/01.exercises/10.solution.actions/db/ship-api.js b/exercises/01.exercises/10.solution.actions/db/ship-api.js deleted file mode 100644 index 4f56d2f..0000000 --- a/exercises/01.exercises/10.solution.actions/db/ship-api.js +++ /dev/null @@ -1,47 +0,0 @@ -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/01.exercises/10.solution.actions/db/ships.json b/exercises/01.exercises/10.solution.actions/db/ships.json deleted file mode 100644 index 99a462f..0000000 --- a/exercises/01.exercises/10.solution.actions/db/ships.json +++ /dev/null @@ -1,309 +0,0 @@ -[ - { - "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/01.exercises/10.solution.actions/dev.js b/exercises/01.exercises/10.solution.actions/dev.js deleted file mode 100644 index b10826e..0000000 --- a/exercises/01.exercises/10.solution.actions/dev.js +++ /dev/null @@ -1,58 +0,0 @@ -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/01.exercises/10.solution.actions/package-lock.json b/exercises/01.exercises/10.solution.actions/package-lock.json deleted file mode 100644 index babe051..0000000 --- a/exercises/01.exercises/10.solution.actions/package-lock.json +++ /dev/null @@ -1,1298 +0,0 @@ -{ - "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/01.exercises/10.solution.actions/package.json b/exercises/01.exercises/10.solution.actions/package.json deleted file mode 100644 index 9b5976e..0000000 --- a/exercises/01.exercises/10.solution.actions/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "exercises__sep__01.exercises__sep__10.solution.actions", - "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/01.exercises/10.solution.actions/public/favicon.ico b/exercises/01.exercises/10.solution.actions/public/favicon.ico deleted file mode 100644 index 890afb6..0000000 Binary files a/exercises/01.exercises/10.solution.actions/public/favicon.ico and /dev/null differ diff --git a/exercises/01.exercises/10.solution.actions/public/favicon.svg b/exercises/01.exercises/10.solution.actions/public/favicon.svg deleted file mode 100644 index 67401c5..0000000 --- a/exercises/01.exercises/10.solution.actions/public/favicon.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/exercises/01.exercises/10.solution.actions/server/rsc-loader.js b/exercises/01.exercises/10.solution.actions/server/rsc-loader.js deleted file mode 100644 index 836ca6f..0000000 --- a/exercises/01.exercises/10.solution.actions/server/rsc-loader.js +++ /dev/null @@ -1,23 +0,0 @@ -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/01.exercises/10.solution.actions/server/rsc.js b/exercises/01.exercises/10.solution.actions/server/rsc.js deleted file mode 100644 index 61dd51e..0000000 --- a/exercises/01.exercises/10.solution.actions/server/rsc.js +++ /dev/null @@ -1,79 +0,0 @@ -import bodyParser from 'body-parser' -import busboy from 'busboy' -import closeWithGrace from 'close-with-grace' -import compress from 'compression' -import express from 'express' -import { createElement as h } from 'react' -import { - decodeReplyFromBusboy, - 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 - -async function renderApp(res, returnValue) { - const shipId = res.req.params.shipId || null - const search = res.req.query.search || '' - res.set('x-location', res.req.url) - shipDataStorage.run({ shipId, search }, () => { - const root = h(Document) - const payload = { returnValue, root } - const { pipe } = renderToPipeableStream(payload, moduleBasePath) - pipe(res) - }) -} - -app.get('/:shipId?', async function (req, res) { - await renderApp(res, null) -}) - -app.post('/:shipId?', bodyParser.text(), async function (req, res) { - const serverReference = req.get('rsc-action') - // This is the client-side case - const [filepath, name] = serverReference.split('#') - const action = (await import(filepath))[name] - // Validate that this is actually a function we intended to expose and - // not the client trying to invoke arbitrary functions. In a real app, - // you'd have a manifest verifying this before even importing it. - if (action.$$typeof !== Symbol.for('react.server.reference')) { - throw new Error('Invalid action') - } - - const bb = busboy({ headers: req.headers }) - const reply = decodeReplyFromBusboy(bb, moduleBasePath) - req.pipe(bb) - const args = await reply - const result = action.apply(null, args) - try { - // Wait for any mutations - await result - } catch (x) { - // We handle the error on the client - } - // Refresh the client and return the value - await renderApp(res, result) -}) - -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/01.exercises/10.solution.actions/server/ssr.js b/exercises/01.exercises/10.solution.actions/server/ssr.js deleted file mode 100644 index ef8c222..0000000 --- a/exercises/01.exercises/10.solution.actions/server/ssr.js +++ /dev/null @@ -1,156 +0,0 @@ -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('/', (req, res) => res.status(200).end()) - -app.use(express.static('public')) -app.use('/js/src', express.static('src')) - -// we have to server this file from our own server so dynamic imports are -// relative to our own server (this module is what loads client-side modules!) -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) { - const promiseForData = request( - { - host: RSC_ORIGIN.hostname, - port: RSC_ORIGIN.port, - method: req.method, - path: req.url, - headers: req.headers, - }, - req, - ) - - if (req.accepts('text/html')) { - try { - res.set('Content-type', 'text/html') - const rscResponse = await promiseForData - const moduleBaseURL = '/js/src' - - 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) - }) - - 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/01.exercises/10.solution.actions/src/actions.js b/exercises/01.exercises/10.solution.actions/src/actions.js deleted file mode 100644 index 23f42f3..0000000 --- a/exercises/01.exercises/10.solution.actions/src/actions.js +++ /dev/null @@ -1,16 +0,0 @@ -'use server' - -import * as db from '../db/ship-api.js' - -export async function updateShipName(previousState, formData) { - const shouldSucceed = Math.random() > 0.5 - if (shouldSucceed) { - await db.updateShipName({ - shipId: formData.get('shipId'), - shipName: formData.get('shipName'), - }) - return { status: 'success', message: 'Success!' } - } else { - return { status: 'error', message: 'Error! Unlucky' } - } -} diff --git a/exercises/01.exercises/10.solution.actions/src/app.js b/exercises/01.exercises/10.solution.actions/src/app.js deleted file mode 100644 index 0fae21a..0000000 --- a/exercises/01.exercises/10.solution.actions/src/app.js +++ /dev/null @@ -1,79 +0,0 @@ -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))), - ) -} - -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/01.exercises/10.solution.actions/src/edit-text.js b/exercises/01.exercises/10.solution.actions/src/edit-text.js deleted file mode 100644 index 8d1dfc4..0000000 --- a/exercises/01.exercises/10.solution.actions/src/edit-text.js +++ /dev/null @@ -1,100 +0,0 @@ -'use client' - -import { createElement as h, useRef, useState, useActionState } from 'react' -import { flushSync } from 'react-dom' - -const inheritStyles = { - fontSize: 'inherit', - fontStyle: 'inherit', - fontWeight: 'inherit', - fontFamily: 'inherit', - textAlign: 'inherit', -} - -export function EditableText({ id, shipId, action, initialValue = '' }) { - const [edit, setEdit] = useState(false) - const [value, setValue] = useState(initialValue) - const [formState, formAction, isPending] = useActionState(action) - const inputRef = useRef(null) - const buttonRef = useRef(null) - return edit - ? h( - 'form', - { - action: formAction, - onSubmit: () => { - setValue(inputRef.current?.value ?? '') - }, - }, - h('input', { - type: 'hidden', - name: 'shipId', - value: shipId, - }), - h('input', { - required: true, - ref: inputRef, - type: 'text', - id: id, - 'aria-label': 'Ship Name', - name: 'shipName', - defaultValue: value, - style: { - border: 'none', - background: 'none', - ...inheritStyles, - }, - onKeyDown: event => { - if (event.key === 'Escape') { - flushSync(() => { - setEdit(false) - }) - buttonRef.current?.focus() - } - }, - }), - h( - 'div', - { style: { display: 'flex', gap: 2, justifyContent: 'center' } }, - h( - 'button', - { type: 'button', onClick: () => setEdit(false) }, - 'Done editing', - ), - h('button', { type: 'submit' }, isPending ? '...' : 'Submit'), - ), - formState - ? h( - 'div', - { - style: { - color: formState.status === 'error' ? 'red' : 'green', - fontSize: '0.75rem', - fontWeight: 'normal', - }, - }, - formState.message, - ) - : null, - ) - : h( - 'button', - { - 'aria-label': 'Ship Name', - ref: buttonRef, - type: 'button', - style: { - border: 'none', - background: 'none', - ...inheritStyles, - }, - onClick: () => { - flushSync(() => { - setEdit(true) - }) - inputRef.current?.select() - }, - }, - value || 'Edit', - ) -} diff --git a/exercises/01.exercises/10.solution.actions/src/error-boundary.js b/exercises/01.exercises/10.solution.actions/src/error-boundary.js deleted file mode 100644 index 9a93e62..0000000 --- a/exercises/01.exercises/10.solution.actions/src/error-boundary.js +++ /dev/null @@ -1,4 +0,0 @@ -// https://github.com/bvaughn/react-error-boundary/issues/182 -'use client' - -export { ErrorBoundary } from 'react-error-boundary' diff --git a/exercises/01.exercises/10.solution.actions/src/index.js b/exercises/01.exercises/10.solution.actions/src/index.js deleted file mode 100644 index b0e7fe3..0000000 --- a/exercises/01.exercises/10.solution.actions/src/index.js +++ /dev/null @@ -1,155 +0,0 @@ -import { - createElement as h, - startTransition, - use, - useDeferredValue, - useEffect, - useReducer, - useRef, - useState, - useTransition, -} from 'react' -import { hydrateRoot } 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' - -function generateKey() { - return Date.now().toString(36) + Math.random().toString(36).slice(2) -} - -function updateContentKey() { - console.error('updateContentKey called before it was set!') -} -const contentCache = new Map() - -function createFromFetch(fetchPromise) { - return RSC.createFromFetch(fetchPromise, { moduleBaseURL, callServer }) -} - -async function callServer(id, args) { - // using the global location to avoid a stale closure over the location - const fetchPromise = fetch(getGlobalLocation(), { - method: 'POST', - headers: { Accept: 'text/x-component', 'rsc-action': id }, - body: await RSC.encodeReply(args), - }) - const actionResponsePromise = createFromFetch(fetchPromise) - const newContentKey = generateKey() - contentCache.set(newContentKey, actionResponsePromise) - updateContentKey(newContentKey) - const { returnValue } = await actionResponsePromise - return returnValue -} - -const initialLocation = getGlobalLocation() -const initialContentPromise = createFromFetch(fetchContent(initialLocation)) - -let initialContentKey = window.history.state?.key -if (!initialContentKey) { - initialContentKey = generateKey() - window.history.replaceState({ key: initialContentKey }, '') -} -contentCache.set(initialContentKey, initialContentPromise) - -export function Root() { - const [, forceRender] = useReducer(() => Symbol(), Symbol()) - const latestNav = useRef(null) - const [nextLocation, setNextLocation] = useState(getGlobalLocation) - const [contentKey, setContentKey] = useState(initialContentKey) - const [isPending, startTransition] = useTransition() - - // update the updateContentKey function to the latest every render - useEffect(() => { - updateContentKey = newContentKey => { - startTransition(() => setContentKey(newContentKey)) - } - }) - - const location = useDeferredValue(nextLocation) - const contentPromise = contentCache.get(contentKey) - - useEffect(() => { - function handlePopState() { - const nextLocation = getGlobalLocation() - setNextLocation(nextLocation) - const historyKey = window.history.state?.key ?? generateKey() - - const thisNav = Symbol(`Nav for ${historyKey}`) - latestNav.current = thisNav - - let nextContentPromise - const fetchPromise = fetchContent(nextLocation) - // create a promise chain that resolves when the stream is completely consumed - fetchPromise - // clone the response so createFromFetch can use it (otherwise we lock the reader) - // and wait for the text to be consumed so we know the stream is finished - .then(response => response.clone().text()) - .then(() => { - contentCache.set(historyKey, nextContentPromise) - if (thisNav === latestNav.current) { - // trigger a rerender now that the updated content is in the cache - startTransition(() => forceRender()) - } - }) - nextContentPromise = createFromFetch(fetchPromise) - - if (!contentCache.has(historyKey)) { - // if we don't have this key in the cache already, set it now - contentCache.set(historyKey, nextContentPromise) - } - - updateContentKey(historyKey) - } - window.addEventListener('popstate', handlePopState) - return () => window.removeEventListener('popstate', handlePopState) - }, []) - - async function navigate(nextLocation, { replace = false, contentKey } = {}) { - setNextLocation(nextLocation) - const thisNav = Symbol() - latestNav.current = thisNav - - const newContentKey = contentKey ?? generateKey() - const nextContentPromise = createFromFetch( - fetchContent(nextLocation).then(response => { - if (thisNav !== latestNav.current) return - const newLocation = response.headers.get('x-location') - if (replace) { - window.history.replaceState({ key: newContentKey }, '', newLocation) - } else { - window.history.pushState({ key: newContentKey }, '', newLocation) - } - return response - }), - ) - - contentCache.set(newContentKey, nextContentPromise) - updateContentKey(newContentKey) - } - - return h( - RouterContext.Provider, - { - value: { - location, - nextLocation: isPending ? nextLocation : location, - navigate, - isPending, - }, - }, - use(contentPromise).root, - ) -} - -startTransition(() => { - hydrateRoot(document, h(Root)) -}) diff --git a/exercises/01.exercises/10.solution.actions/src/router.js b/exercises/01.exercises/10.solution.actions/src/router.js deleted file mode 100644 index 248e678..0000000 --- a/exercises/01.exercises/10.solution.actions/src/router.js +++ /dev/null @@ -1,34 +0,0 @@ -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/01.exercises/10.solution.actions/src/ship-details-pending.js b/exercises/01.exercises/10.solution.actions/src/ship-details-pending.js deleted file mode 100644 index 0d98be6..0000000 --- a/exercises/01.exercises/10.solution.actions/src/ship-details-pending.js +++ /dev/null @@ -1,21 +0,0 @@ -'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/01.exercises/10.solution.actions/src/ship-details.js b/exercises/01.exercises/10.solution.actions/src/ship-details.js deleted file mode 100644 index 7360aae..0000000 --- a/exercises/01.exercises/10.solution.actions/src/ship-details.js +++ /dev/null @@ -1,115 +0,0 @@ -import { createElement as h } from 'react' -import { getShip } from '../db/ship-api.js' -import { shipDataStorage } from '../server/async-storage.js' -import { updateShipName } from './actions.js' -import { EditableText } from './edit-text.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, - h(EditableText, { - key: shipId, - shipId, - action: updateShipName, - initialValue: 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/01.exercises/10.solution.actions/src/ship-search-results.js b/exercises/01.exercises/10.solution.actions/src/ship-search-results.js deleted file mode 100644 index fb4f956..0000000 --- a/exercises/01.exercises/10.solution.actions/src/ship-search-results.js +++ /dev/null @@ -1,43 +0,0 @@ -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 { SelectShipLink } 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( - SelectShipLink, - { 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/01.exercises/10.solution.actions/src/ship-search.js b/exercises/01.exercises/10.solution.actions/src/ship-search.js deleted file mode 100644 index b7f6899..0000000 --- a/exercises/01.exercises/10.solution.actions/src/ship-search.js +++ /dev/null @@ -1,68 +0,0 @@ -'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 SelectShipLink({ 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/01.exercises/10.solution.actions/src/spin-delay.js b/exercises/01.exercises/10.solution.actions/src/spin-delay.js deleted file mode 100644 index e5fd221..0000000 --- a/exercises/01.exercises/10.solution.actions/src/spin-delay.js +++ /dev/null @@ -1,35 +0,0 @@ -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' -} diff --git a/exercises/01.exercises/FINISHED.mdx b/exercises/01.exercises/FINISHED.mdx deleted file mode 100644 index 24b25e7..0000000 --- a/exercises/01.exercises/FINISHED.mdx +++ /dev/null @@ -1 +0,0 @@ -# Start diff --git a/exercises/01.exercises/README.mdx b/exercises/01.exercises/README.mdx deleted file mode 100644 index 24b25e7..0000000 --- a/exercises/01.exercises/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# Start diff --git a/exercises/01.exercises/01.problem.ssr/.gitignore b/exercises/01.init/01.problem.static/.gitignore similarity index 100% rename from exercises/01.exercises/01.problem.ssr/.gitignore rename to exercises/01.init/01.problem.static/.gitignore diff --git a/exercises/01.init/01.problem.static/README.mdx b/exercises/01.init/01.problem.static/README.mdx new file mode 100644 index 0000000..7622fdb --- /dev/null +++ b/exercises/01.init/01.problem.static/README.mdx @@ -0,0 +1,21 @@ +# Static React App + + + +๐Ÿ‘จโ€๐Ÿ’ผ We've got an app all ready to go, we just need things wired up so the hono.js +app serves our static assets properly as well as our endpoint for data. + +You'll find the database in and the db +client in . + +Your job here is to get the hono.js app to serve the static assets and serve the +ship data from the database. Do this in . + +Then fix our `importmap` in so the +browser knows how to resolve imports for our dependencies and add a script tag +to load our from the hono.js server via +`/ui/index.js`. + +๐Ÿ’ฐ I'm going to be **extremely** hand-holdy on this one because this is really +just a warm up for you to get to know the codebase a bit. We'll get to the +React Server Components stuff next. diff --git a/exercises/01.init/01.problem.static/db/ship-api.js b/exercises/01.init/01.problem.static/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/01.init/01.problem.static/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/01.init/01.problem.static/db/ships.json b/exercises/01.init/01.problem.static/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/01.init/01.problem.static/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/01.init/01.problem.static/package.json b/exercises/01.init/01.problem.static/package.json new file mode 100644 index 0000000..bf7e38a --- /dev/null +++ b/exercises/01.init/01.problem.static/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__01.init__sep__01.problem.static", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/01.init/01.problem.static/public/favicon.ico b/exercises/01.init/01.problem.static/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/01.init/01.problem.static/public/favicon.ico differ diff --git a/exercises/01.init/01.problem.static/public/favicon.svg b/exercises/01.init/01.problem.static/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/01.init/01.problem.static/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/01.init/01.problem.static/public/iframe-sync.js b/exercises/01.init/01.problem.static/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/01.init/01.problem.static/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/01.problem.ssr/public/img/broken-ship.webp b/exercises/01.init/01.problem.static/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/broken-ship.webp rename to exercises/01.init/01.problem.static/public/img/broken-ship.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/fallback-ship.png b/exercises/01.init/01.problem.static/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/fallback-ship.png rename to exercises/01.init/01.problem.static/public/img/fallback-ship.png diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/0268fc4817ad1.webp b/exercises/01.init/01.problem.static/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/0268fc4817ad1.webp rename to exercises/01.init/01.problem.static/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/1ae7b4b92036b.webp b/exercises/01.init/01.problem.static/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/1ae7b4b92036b.webp rename to exercises/01.init/01.problem.static/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/1ff1991efe029.webp b/exercises/01.init/01.problem.static/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/1ff1991efe029.webp rename to exercises/01.init/01.problem.static/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/3ba8aa65ffe6c.webp b/exercises/01.init/01.problem.static/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/01.init/01.problem.static/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/441f7092a8d44.webp b/exercises/01.init/01.problem.static/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/441f7092a8d44.webp rename to exercises/01.init/01.problem.static/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/5c13d8b28a14a.webp b/exercises/01.init/01.problem.static/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/5c13d8b28a14a.webp rename to exercises/01.init/01.problem.static/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/627c497212456.webp b/exercises/01.init/01.problem.static/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/627c497212456.webp rename to exercises/01.init/01.problem.static/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/670003aed3795.webp b/exercises/01.init/01.problem.static/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/670003aed3795.webp rename to exercises/01.init/01.problem.static/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/6c86fca8b9086.webp b/exercises/01.init/01.problem.static/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/6c86fca8b9086.webp rename to exercises/01.init/01.problem.static/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/6f375578ead88.webp b/exercises/01.init/01.problem.static/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/6f375578ead88.webp rename to exercises/01.init/01.problem.static/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/ab267a5984523.webp b/exercises/01.init/01.problem.static/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/ab267a5984523.webp rename to exercises/01.init/01.problem.static/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/b442531ea32b2.webp b/exercises/01.init/01.problem.static/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/b442531ea32b2.webp rename to exercises/01.init/01.problem.static/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/bc4cbadf89bd3.webp b/exercises/01.init/01.problem.static/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/bc4cbadf89bd3.webp rename to exercises/01.init/01.problem.static/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/cb03cc4e5717e.webp b/exercises/01.init/01.problem.static/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/cb03cc4e5717e.webp rename to exercises/01.init/01.problem.static/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/cfd10fcd2de6c.webp b/exercises/01.init/01.problem.static/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/cfd10fcd2de6c.webp rename to exercises/01.init/01.problem.static/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/d3b8aa65ffe6c.webp b/exercises/01.init/01.problem.static/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/01.init/01.problem.static/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/d486d48b82b81.webp b/exercises/01.init/01.problem.static/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/d486d48b82b81.webp rename to exercises/01.init/01.problem.static/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/e92cefe4f6727.webp b/exercises/01.init/01.problem.static/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/e92cefe4f6727.webp rename to exercises/01.init/01.problem.static/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/ec7a3f950f99f.webp b/exercises/01.init/01.problem.static/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/ec7a3f950f99f.webp rename to exercises/01.init/01.problem.static/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/f3d9a88e1c234.webp b/exercises/01.init/01.problem.static/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/f3d9a88e1c234.webp rename to exercises/01.init/01.problem.static/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/01.problem.ssr/public/img/ships/fdc13cb488bf1.webp b/exercises/01.init/01.problem.static/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/img/ships/fdc13cb488bf1.webp rename to exercises/01.init/01.problem.static/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/01.init/01.problem.static/public/index.html b/exercises/01.init/01.problem.static/public/index.html new file mode 100644 index 0000000..e6145aa --- /dev/null +++ b/exercises/01.init/01.problem.static/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/01.problem.ssr/public/style.css b/exercises/01.init/01.problem.static/public/style.css similarity index 100% rename from exercises/01.exercises/01.problem.ssr/public/style.css rename to exercises/01.init/01.problem.static/public/style.css diff --git a/exercises/01.init/01.problem.static/server/app.js b/exercises/01.init/01.problem.static/server/app.js new file mode 100644 index 0000000..9abf6fc --- /dev/null +++ b/exercises/01.init/01.problem.static/server/app.js @@ -0,0 +1,82 @@ +// ๐Ÿ’ฐ you're gonna need this +// import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +// ๐Ÿ’ฐ you're gonna need this +// import { serveStatic } from '@hono/node-server/serve-static' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +// ๐Ÿ’ฐ you're gonna need these: +// import { getShip, searchShips } from '../db/ship-api.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +// ๐Ÿจ add a static hono.js handler for the public folder, but leave out the index.html +// ๐Ÿ’ฐ app.use('/*', serveStatic({ root: './public', index: '' })) + +// ๐Ÿจ add a handler for requests to '/ui' that serves static files from 'ui' +// ๐Ÿ’ฐ +// app.use( +// '/ui/*', +// serveStatic({ +// root: './ui', +// onNotFound: (path, context) => context.text('File not found', 404), +// rewriteRequestPath: path => path.replace('/ui', ''), +// }), +// ) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +// ๐Ÿจ add an API endpoint to get data for our page: +// ๐Ÿ’ฐ +// app.get('/api/:shipId?', async context => { +// const shipId = context.req.param('shipId') || null +// const search = context.req.query('search') || '' +// const ship = shipId ? await getShip({ shipId }) : null +// const shipResults = await searchShips({ search }) +// const data = { shipId, search, ship, shipResults } +// return context.json(data) +// }) + +// ๐Ÿจ add a handler for '/:shipId?' which means the ship is optional +// ๐Ÿจ set the response Content-type to 'text/html' and send the file in public called index.html +// ๐Ÿ’ฐ +// app.get('/:shipId?', async context => { +// const html = await readFile('./public/index.html', 'utf8') +// return context.html(html, 200) +// }) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.init/01.problem.static/tests/playwright.config.js b/exercises/01.init/01.problem.static/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/01.init/01.problem.static/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/01.init/01.problem.static/tests/solution.test.js b/exercises/01.init/01.problem.static/tests/solution.test.js new file mode 100644 index 0000000..c1ac38f --- /dev/null +++ b/exercises/01.init/01.problem.static/tests/solution.test.js @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + await filterInput.press('Enter') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + await page.waitForLoadState('networkidle') + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/01.init/01.problem.static/ui/app.js b/exercises/01.init/01.problem.static/ui/app.js new file mode 100644 index 0000000..100fa6f --- /dev/null +++ b/exercises/01.init/01.problem.static/ui/app.js @@ -0,0 +1,37 @@ +import { Fragment, createElement as h } from 'react' +import { ShipDetails } from './ship-details.js' +import { SearchResults } from './ship-search-results.js' + +export function App({ shipId, search, ship, shipResults }) { + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h( + Fragment, + null, + h( + 'form', + {}, + h('input', { + name: 'search', + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + autoFocus: true, + }), + ), + h('ul', null, h(SearchResults, { shipId, search, shipResults })), + ), + ), + h( + 'div', + { className: 'details' }, + shipId + ? h(ShipDetails, { ship }) + : h('p', null, 'Select a ship from the list to see details'), + ), + ) +} diff --git a/exercises/01.exercises/04.problem.async-components/src/img-utils.js b/exercises/01.init/01.problem.static/ui/img-utils.js similarity index 100% rename from exercises/01.exercises/04.problem.async-components/src/img-utils.js rename to exercises/01.init/01.problem.static/ui/img-utils.js diff --git a/exercises/01.init/01.problem.static/ui/index.js b/exercises/01.init/01.problem.static/ui/index.js new file mode 100644 index 0000000..2ee04e1 --- /dev/null +++ b/exercises/01.init/01.problem.static/ui/index.js @@ -0,0 +1,34 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +import { App } from './app.js' +import { shipFallbackSrc } from './img-utils.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +const initialLocation = getGlobalLocation() +const initialDataPromise = fetch(`/api${initialLocation}`).then((r) => r.json()) + +function Root() { + const { shipId, search, ship, shipResults } = use(initialDataPromise) + return h(App, { shipId, search, ship, shipResults }) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h(Root), + ), + ), + ) +}) diff --git a/exercises/01.init/01.problem.static/ui/ship-details.js b/exercises/01.init/01.problem.static/ui/ship-details.js new file mode 100644 index 0000000..21b4ce1 --- /dev/null +++ b/exercises/01.init/01.problem.static/ui/ship-details.js @@ -0,0 +1,43 @@ +import { createElement as h } from 'react' +import { getImageUrlForShip } from './img-utils.js' + +export function ShipDetails({ ship }) { + const shipImgSrc = getImageUrlForShip(ship.id, { size: 200 }) + return h( + 'div', + { className: 'ship-info' }, + h( + 'div', + { className: 'ship-info__img-wrapper' }, + h('img', { 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.'), + ), + ) +} diff --git a/exercises/01.init/01.problem.static/ui/ship-search-results.js b/exercises/01.init/01.problem.static/ui/ship-search-results.js new file mode 100644 index 0000000..726fbb9 --- /dev/null +++ b/exercises/01.init/01.problem.static/ui/ship-search-results.js @@ -0,0 +1,29 @@ +import { createElement as h } from 'react' +import { getImageUrlForShip } from './img-utils.js' + +export function SearchResults({ shipId: currentShipId, shipResults, search }) { + return shipResults.ships.map((ship) => { + const href = [ + `/${ship.id}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h( + 'li', + { key: ship.name }, + h( + 'a', + { + href, + style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, + }, + h('img', { + src: getImageUrlForShip(ship.id, { size: 20 }), + alt: ship.name, + }), + ship.name, + ), + ) + }) +} diff --git a/exercises/01.exercises/01.solution.ssr/.gitignore b/exercises/01.init/01.solution.static/.gitignore similarity index 100% rename from exercises/01.exercises/01.solution.ssr/.gitignore rename to exercises/01.init/01.solution.static/.gitignore diff --git a/exercises/01.init/01.solution.static/README.mdx b/exercises/01.init/01.solution.static/README.mdx new file mode 100644 index 0000000..e177150 --- /dev/null +++ b/exercises/01.init/01.solution.static/README.mdx @@ -0,0 +1,7 @@ +# Static React App + + + +๐Ÿ‘จโ€๐Ÿ’ผ Great work! Hopefully now you have a good idea of how the app works +currently. I know the lack of JSX support is a little much, but don't worry, you +got this! diff --git a/exercises/01.init/01.solution.static/db/ship-api.js b/exercises/01.init/01.solution.static/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/01.init/01.solution.static/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/01.init/01.solution.static/db/ships.json b/exercises/01.init/01.solution.static/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/01.init/01.solution.static/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/01.init/01.solution.static/package.json b/exercises/01.init/01.solution.static/package.json new file mode 100644 index 0000000..3e97bd8 --- /dev/null +++ b/exercises/01.init/01.solution.static/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__01.init__sep__01.solution.static", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/01.init/01.solution.static/public/favicon.ico b/exercises/01.init/01.solution.static/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/01.init/01.solution.static/public/favicon.ico differ diff --git a/exercises/01.init/01.solution.static/public/favicon.svg b/exercises/01.init/01.solution.static/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/01.init/01.solution.static/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/01.init/01.solution.static/public/iframe-sync.js b/exercises/01.init/01.solution.static/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/01.init/01.solution.static/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/01.solution.ssr/public/img/broken-ship.webp b/exercises/01.init/01.solution.static/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/broken-ship.webp rename to exercises/01.init/01.solution.static/public/img/broken-ship.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/fallback-ship.png b/exercises/01.init/01.solution.static/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/fallback-ship.png rename to exercises/01.init/01.solution.static/public/img/fallback-ship.png diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/0268fc4817ad1.webp b/exercises/01.init/01.solution.static/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/0268fc4817ad1.webp rename to exercises/01.init/01.solution.static/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/1ae7b4b92036b.webp b/exercises/01.init/01.solution.static/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/1ae7b4b92036b.webp rename to exercises/01.init/01.solution.static/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/1ff1991efe029.webp b/exercises/01.init/01.solution.static/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/1ff1991efe029.webp rename to exercises/01.init/01.solution.static/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/3ba8aa65ffe6c.webp b/exercises/01.init/01.solution.static/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/01.init/01.solution.static/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/441f7092a8d44.webp b/exercises/01.init/01.solution.static/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/441f7092a8d44.webp rename to exercises/01.init/01.solution.static/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/5c13d8b28a14a.webp b/exercises/01.init/01.solution.static/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/5c13d8b28a14a.webp rename to exercises/01.init/01.solution.static/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/627c497212456.webp b/exercises/01.init/01.solution.static/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/627c497212456.webp rename to exercises/01.init/01.solution.static/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/670003aed3795.webp b/exercises/01.init/01.solution.static/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/670003aed3795.webp rename to exercises/01.init/01.solution.static/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/6c86fca8b9086.webp b/exercises/01.init/01.solution.static/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/6c86fca8b9086.webp rename to exercises/01.init/01.solution.static/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/6f375578ead88.webp b/exercises/01.init/01.solution.static/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/6f375578ead88.webp rename to exercises/01.init/01.solution.static/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/ab267a5984523.webp b/exercises/01.init/01.solution.static/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/ab267a5984523.webp rename to exercises/01.init/01.solution.static/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/b442531ea32b2.webp b/exercises/01.init/01.solution.static/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/b442531ea32b2.webp rename to exercises/01.init/01.solution.static/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/bc4cbadf89bd3.webp b/exercises/01.init/01.solution.static/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/bc4cbadf89bd3.webp rename to exercises/01.init/01.solution.static/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/cb03cc4e5717e.webp b/exercises/01.init/01.solution.static/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/cb03cc4e5717e.webp rename to exercises/01.init/01.solution.static/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/cfd10fcd2de6c.webp b/exercises/01.init/01.solution.static/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/cfd10fcd2de6c.webp rename to exercises/01.init/01.solution.static/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/d3b8aa65ffe6c.webp b/exercises/01.init/01.solution.static/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/01.init/01.solution.static/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/d486d48b82b81.webp b/exercises/01.init/01.solution.static/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/d486d48b82b81.webp rename to exercises/01.init/01.solution.static/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/e92cefe4f6727.webp b/exercises/01.init/01.solution.static/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/e92cefe4f6727.webp rename to exercises/01.init/01.solution.static/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/ec7a3f950f99f.webp b/exercises/01.init/01.solution.static/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/ec7a3f950f99f.webp rename to exercises/01.init/01.solution.static/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/f3d9a88e1c234.webp b/exercises/01.init/01.solution.static/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/f3d9a88e1c234.webp rename to exercises/01.init/01.solution.static/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/01.solution.ssr/public/img/ships/fdc13cb488bf1.webp b/exercises/01.init/01.solution.static/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/img/ships/fdc13cb488bf1.webp rename to exercises/01.init/01.solution.static/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/01.init/01.solution.static/public/index.html b/exercises/01.init/01.solution.static/public/index.html new file mode 100644 index 0000000..d59fd1a --- /dev/null +++ b/exercises/01.init/01.solution.static/public/index.html @@ -0,0 +1,25 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/01.solution.ssr/public/style.css b/exercises/01.init/01.solution.static/public/style.css similarity index 100% rename from exercises/01.exercises/01.solution.ssr/public/style.css rename to exercises/01.init/01.solution.static/public/style.css diff --git a/exercises/01.init/01.solution.static/server/app.js b/exercises/01.init/01.solution.static/server/app.js new file mode 100644 index 0000000..92872a8 --- /dev/null +++ b/exercises/01.init/01.solution.static/server/app.js @@ -0,0 +1,71 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { getShip, searchShips } from '../db/ship-api.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/api/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const ship = shipId ? await getShip({ shipId }) : null + const shipResults = await searchShips({ search }) + const data = { shipId, search, ship, shipResults } + return context.json(data) +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.init/01.solution.static/tests/playwright.config.js b/exercises/01.init/01.solution.static/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/01.init/01.solution.static/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/01.init/01.solution.static/tests/solution.test.js b/exercises/01.init/01.solution.static/tests/solution.test.js new file mode 100644 index 0000000..c1ac38f --- /dev/null +++ b/exercises/01.init/01.solution.static/tests/solution.test.js @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + await filterInput.press('Enter') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + await page.waitForLoadState('networkidle') + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/01.init/01.solution.static/ui/app.js b/exercises/01.init/01.solution.static/ui/app.js new file mode 100644 index 0000000..100fa6f --- /dev/null +++ b/exercises/01.init/01.solution.static/ui/app.js @@ -0,0 +1,37 @@ +import { Fragment, createElement as h } from 'react' +import { ShipDetails } from './ship-details.js' +import { SearchResults } from './ship-search-results.js' + +export function App({ shipId, search, ship, shipResults }) { + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h( + Fragment, + null, + h( + 'form', + {}, + h('input', { + name: 'search', + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + autoFocus: true, + }), + ), + h('ul', null, h(SearchResults, { shipId, search, shipResults })), + ), + ), + h( + 'div', + { className: 'details' }, + shipId + ? h(ShipDetails, { ship }) + : h('p', null, 'Select a ship from the list to see details'), + ), + ) +} diff --git a/exercises/01.exercises/04.solution.async-components/src/img-utils.js b/exercises/01.init/01.solution.static/ui/img-utils.js similarity index 100% rename from exercises/01.exercises/04.solution.async-components/src/img-utils.js rename to exercises/01.init/01.solution.static/ui/img-utils.js diff --git a/exercises/01.init/01.solution.static/ui/index.js b/exercises/01.init/01.solution.static/ui/index.js new file mode 100644 index 0000000..2ee04e1 --- /dev/null +++ b/exercises/01.init/01.solution.static/ui/index.js @@ -0,0 +1,34 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +import { App } from './app.js' +import { shipFallbackSrc } from './img-utils.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +const initialLocation = getGlobalLocation() +const initialDataPromise = fetch(`/api${initialLocation}`).then((r) => r.json()) + +function Root() { + const { shipId, search, ship, shipResults } = use(initialDataPromise) + return h(App, { shipId, search, ship, shipResults }) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h(Root), + ), + ), + ) +}) diff --git a/exercises/01.init/01.solution.static/ui/ship-details.js b/exercises/01.init/01.solution.static/ui/ship-details.js new file mode 100644 index 0000000..21b4ce1 --- /dev/null +++ b/exercises/01.init/01.solution.static/ui/ship-details.js @@ -0,0 +1,43 @@ +import { createElement as h } from 'react' +import { getImageUrlForShip } from './img-utils.js' + +export function ShipDetails({ ship }) { + const shipImgSrc = getImageUrlForShip(ship.id, { size: 200 }) + return h( + 'div', + { className: 'ship-info' }, + h( + 'div', + { className: 'ship-info__img-wrapper' }, + h('img', { 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.'), + ), + ) +} diff --git a/exercises/01.init/01.solution.static/ui/ship-search-results.js b/exercises/01.init/01.solution.static/ui/ship-search-results.js new file mode 100644 index 0000000..726fbb9 --- /dev/null +++ b/exercises/01.init/01.solution.static/ui/ship-search-results.js @@ -0,0 +1,29 @@ +import { createElement as h } from 'react' +import { getImageUrlForShip } from './img-utils.js' + +export function SearchResults({ shipId: currentShipId, shipResults, search }) { + return shipResults.ships.map((ship) => { + const href = [ + `/${ship.id}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h( + 'li', + { key: ship.name }, + h( + 'a', + { + href, + style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, + }, + h('img', { + src: getImageUrlForShip(ship.id, { size: 20 }), + alt: ship.name, + }), + ship.name, + ), + ) + }) +} diff --git a/exercises/01.init/FINISHED.mdx b/exercises/01.init/FINISHED.mdx new file mode 100644 index 0000000..2c98db4 --- /dev/null +++ b/exercises/01.init/FINISHED.mdx @@ -0,0 +1,7 @@ +# Warm Up + + + +๐Ÿ‘จโ€๐Ÿ’ผ Whatever your server-side architecture, every web application will have these +bits. So that was a good place for us to start for the warm up. Don't forget to +write down what you learned! diff --git a/exercises/01.init/README.mdx b/exercises/01.init/README.mdx new file mode 100644 index 0000000..a8838c7 --- /dev/null +++ b/exercises/01.init/README.mdx @@ -0,0 +1,105 @@ +# Warm Up + + + +This first exercise is just a warm up to get you familiar with the application +we're building for today. The key things you need to understand from this +exercise are: + +1. The `public/index.html` is what we want loaded for browser document requests + to our application. +2. The application is served by a [hono.js](https://hono.dev/) server that + serves the static assets and the API endpoint. + +This is not an node/hono.js workshop, so we'll be really hand-holdy on things to +avoid you wasting time acquiring knowledge of things that you didn't come here +to learn. + +That said, because we're not using any build tools, there may be some things +we're doing you may not be familiar with. + +## Import Map + +Because we're not using any tools to bundle imports, we're using native +EcmaScript Modules (ESM). Our code is written to run in two environments in this +project: + +- Node.js +- Browsers + +In Node.js, there is a module resolution algorithm that is used to resolve +modules. In the browser, there is no such algorithm. To make our code work in +both environments, we're using an `importmap` to map the module names to the +actual URLs where the modules can be found. + +Here's an example of an `importmap` script: + +```html filename=public/index.html + + +``` + +```js filename=ui/index.js +// the importmap above will allow the browser to know where to go to get the +// modules when they are imported. +import { createElement as h } from 'react' +import { renderRoot } from 'react-dom/client' + +// because this file was loaded from /ui/index.js, the browser will know to +// import this file from /ui/app.js +import { App } from './app.js' + +// other stuff here... +``` + +You'll notice we're using `esm.sh` to load the modules. This is a service that +provides ESM versions of popular libraries. It bundles them on-demand and +serves them. This works nicely for our build-less setup (though it does mean +you need to be connected to the internet to work through this workshop). + +- [esm.sh](https://esm.sh) +- [MDN `importmap` docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) + +## Hono.js static + +As mentioned, this is not a workshop about hono.js, but we're using it to serve +our static assets and API endpoints. Here's how serving static assets works with +hono.js: + +```js filename=server/app.js +import { serveStatic } from '@hono/node-server/serve-static' +import { Hono } from 'hono' + +const app = new Hono({ strict: true }) + +// this line tells hono.js to serve the files in the public directory when a +// request is made to the root of the server. +app.use('/*', serveStatic({ root: './public' })) +``` + +- [`serve-static`](https://hono.dev/getting-started/nodejs#serve-static-files). + +## Hono.js Route Patterns + +Hono.js has a special format for defining routes that can be used to match +patterns in the URL. Here's an example of a route pattern: + +```js filename=server/app.js +app.get('/api/:id?', (req, res) => { + res.json({ id: req.params.id }) +}) +``` + +In this example, the `:id?` is a pattern that will match any URL that starts +with `/api/` and has an optional `id` parameter. The `req.params.id` will be +populated with the value of the `id` parameter in the URL. + +- [Hono.js Routing](https://hono.dev/api/routing) diff --git a/exercises/01.exercises/02.problem.server-context/.gitignore b/exercises/02.server-components/01.problem.rsc/.gitignore similarity index 100% rename from exercises/01.exercises/02.problem.server-context/.gitignore rename to exercises/02.server-components/01.problem.rsc/.gitignore diff --git a/exercises/02.server-components/01.problem.rsc/README.mdx b/exercises/02.server-components/01.problem.rsc/README.mdx new file mode 100644 index 0000000..b6e1df6 --- /dev/null +++ b/exercises/02.server-components/01.problem.rsc/README.mdx @@ -0,0 +1,81 @@ +# RSCs + + + +๐Ÿ‘จโ€๐Ÿ’ผ Right now we have to load all the data of our app and pass the result as +props to our components. Additionally, we're sending a bunch of code to the +client to render a page that really doesn't need to be on the client. We want +you to solve both of these problems by using `react-server-dom-esm` which will +allow us to generate serialized JSX on the server and render it in the browser. + +## ๐Ÿฆ‰ `react-server` export + +One important thing to remember is that on the server, React components cannot +use hooks like `useState`, `useEffect`, etc. You should think of RSCs as a +one-time template because they'll never be interactive. + +This can be confusing and even cause problems if you're not careful. That's why +React packages have a special version for your RSC server so you don't make this +mistake. You actually don't have to change your code at all for this to work. +Instead, you change the environment in which your code is running. + +On the server, Node.js has an algorithm to resolve module imports like `react` +and `react-server-dom-esm`. It's a fair bit complex, so to be brief, I'll just +say that in the `package.json` of react packages, you'll find something like +this: + +```json +{ + "exports": { + ".": { + "react-server": "./react.react-server.js", + "default": "./index.js" + }, + "./package.json": "./package.json", + "./jsx-runtime": { + "react-server": "./jsx-runtime.react-server.js", + "default": "./jsx-runtime.js" + }, + "./jsx-dev-runtime": "./jsx-dev-runtime.js" + } +} +``` + +This is configuration for the node resolver so the correct version of React is +resolved based on the environment. In our RSC environment, we want to tell Node +to resolve to the `react-server` export. + +This is done by setting the `--conditions` flag in the `node` command. This flag +is used to set the environment in which the code is running. In our case, we +want to set the environment to `react-server`. + +๐Ÿจ So before you do anything else, update the `dev` script in +the file to add `--conditions=react-server`: + +``` +node --conditions=react-server --watch server/app.js +``` + + + Note: the order of the `--watch` and `--conditions` does matter! Put + conditions first and watch second as it appears above. + + +Once you have that done, you can restart your dev server and for any package +that has a special export for an RSC environment, Node will resolve to that +version of the package. + +๐Ÿ“œ You can learn more about the benefits of this decision from +[the Server Module Conventions RFC](https://github.com/reactjs/rfcs/blob/main/text/0227-server-module-conventions.md) + +## ๐Ÿ‘จโ€๐Ÿ’ผ Continue + +Great, from here, you can proceed to +update to properly map the +`react-server-dom-esm/client` import to the correct URL. Once you have that +done, you can update to switch from an +`/api` route that serves data to an `/rsc` route that serves the RSCs. + +Then finally you'll be able to go to the file +and update that to fetch the RSCs from the server and render them using the +`react-server-dom-esm/client` package. diff --git a/exercises/02.server-components/01.problem.rsc/db/ship-api.js b/exercises/02.server-components/01.problem.rsc/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/02.server-components/01.problem.rsc/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/02.server-components/01.problem.rsc/db/ships.json b/exercises/02.server-components/01.problem.rsc/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/02.server-components/01.problem.rsc/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/02.server-components/01.problem.rsc/package.json b/exercises/02.server-components/01.problem.rsc/package.json new file mode 100644 index 0000000..cc4bbcb --- /dev/null +++ b/exercises/02.server-components/01.problem.rsc/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__02.server-components__sep__01.problem.rsc", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/02.server-components/01.problem.rsc/public/favicon.ico b/exercises/02.server-components/01.problem.rsc/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/02.server-components/01.problem.rsc/public/favicon.ico differ diff --git a/exercises/02.server-components/01.problem.rsc/public/favicon.svg b/exercises/02.server-components/01.problem.rsc/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/02.server-components/01.problem.rsc/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/02.server-components/01.problem.rsc/public/iframe-sync.js b/exercises/02.server-components/01.problem.rsc/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/02.server-components/01.problem.rsc/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/02.problem.server-context/public/img/broken-ship.webp b/exercises/02.server-components/01.problem.rsc/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/broken-ship.webp rename to exercises/02.server-components/01.problem.rsc/public/img/broken-ship.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/fallback-ship.png b/exercises/02.server-components/01.problem.rsc/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/fallback-ship.png rename to exercises/02.server-components/01.problem.rsc/public/img/fallback-ship.png diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/0268fc4817ad1.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/0268fc4817ad1.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/1ae7b4b92036b.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/1ae7b4b92036b.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/1ff1991efe029.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/1ff1991efe029.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/3ba8aa65ffe6c.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/441f7092a8d44.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/441f7092a8d44.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/5c13d8b28a14a.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/5c13d8b28a14a.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/627c497212456.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/627c497212456.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/670003aed3795.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/670003aed3795.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/6c86fca8b9086.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/6c86fca8b9086.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/6f375578ead88.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/6f375578ead88.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/ab267a5984523.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/ab267a5984523.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/b442531ea32b2.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/b442531ea32b2.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/bc4cbadf89bd3.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/bc4cbadf89bd3.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/cb03cc4e5717e.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/cb03cc4e5717e.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/cfd10fcd2de6c.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/cfd10fcd2de6c.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/d3b8aa65ffe6c.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/d486d48b82b81.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/d486d48b82b81.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/e92cefe4f6727.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/e92cefe4f6727.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/ec7a3f950f99f.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/ec7a3f950f99f.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/f3d9a88e1c234.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/f3d9a88e1c234.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/02.problem.server-context/public/img/ships/fdc13cb488bf1.webp b/exercises/02.server-components/01.problem.rsc/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/img/ships/fdc13cb488bf1.webp rename to exercises/02.server-components/01.problem.rsc/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/02.server-components/01.problem.rsc/public/index.html b/exercises/02.server-components/01.problem.rsc/public/index.html new file mode 100644 index 0000000..c31ee15 --- /dev/null +++ b/exercises/02.server-components/01.problem.rsc/public/index.html @@ -0,0 +1,30 @@ + + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/02.problem.server-context/public/style.css b/exercises/02.server-components/01.problem.rsc/public/style.css similarity index 100% rename from exercises/01.exercises/02.problem.server-context/public/style.css rename to exercises/02.server-components/01.problem.rsc/public/style.css diff --git a/exercises/02.server-components/01.problem.rsc/server/app.js b/exercises/02.server-components/01.problem.rsc/server/app.js new file mode 100644 index 0000000..ef016a6 --- /dev/null +++ b/exercises/02.server-components/01.problem.rsc/server/app.js @@ -0,0 +1,90 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +// ๐Ÿ’ฐ you're gonna need this +// import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +// ๐Ÿ’ฐ you'll need these +// import { createElement as h } from 'react' +// import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { getShip, searchShips } from '../db/ship-api.js' +// ๐Ÿ’ฐ you'll want this too: +// import { App } from '../ui/app.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +// ๐Ÿจ change this from /api to /rsc +app.get('/api/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const ship = shipId ? await getShip({ shipId }) : null + const shipResults = await searchShips({ search }) + // ๐Ÿจ rename data to props + const data = { shipId, search, ship, shipResults } + // ๐Ÿ’ฃ remove this return context.json + return context.json(data) + // ๐Ÿจ call renderToPipeableStream from react-server-dom-esm/server + // and pass it the App component and the props + // ๐Ÿ’ฐ remember, we don't have a JSX transformer here, so you'll use + // createElement directly which we aliased to `h` for brevity above. + // ๐Ÿฆ‰ renderToPipeableStream returns an object with a pipe function + // ๐Ÿจ pipe the content through the outgoing response + // ๐Ÿ’ฐ pipe(context.env.outgoing) + // ๐Ÿจ let Hono know we're going to stream on the response + // ๐Ÿ’ฐ return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/02.server-components/01.problem.rsc/tests/playwright.config.js b/exercises/02.server-components/01.problem.rsc/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/02.server-components/01.problem.rsc/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/02.server-components/01.problem.rsc/tests/solution.test.js b/exercises/02.server-components/01.problem.rsc/tests/solution.test.js new file mode 100644 index 0000000..c1ac38f --- /dev/null +++ b/exercises/02.server-components/01.problem.rsc/tests/solution.test.js @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + await filterInput.press('Enter') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + await page.waitForLoadState('networkidle') + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/02.server-components/01.problem.rsc/ui/app.js b/exercises/02.server-components/01.problem.rsc/ui/app.js new file mode 100644 index 0000000..100fa6f --- /dev/null +++ b/exercises/02.server-components/01.problem.rsc/ui/app.js @@ -0,0 +1,37 @@ +import { Fragment, createElement as h } from 'react' +import { ShipDetails } from './ship-details.js' +import { SearchResults } from './ship-search-results.js' + +export function App({ shipId, search, ship, shipResults }) { + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h( + Fragment, + null, + h( + 'form', + {}, + h('input', { + name: 'search', + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + autoFocus: true, + }), + ), + h('ul', null, h(SearchResults, { shipId, search, shipResults })), + ), + ), + h( + 'div', + { className: 'details' }, + shipId + ? h(ShipDetails, { ship }) + : h('p', null, 'Select a ship from the list to see details'), + ), + ) +} diff --git a/exercises/01.exercises/05.problem.bootstrap/src/img-utils.js b/exercises/02.server-components/01.problem.rsc/ui/img-utils.js similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/src/img-utils.js rename to exercises/02.server-components/01.problem.rsc/ui/img-utils.js diff --git a/exercises/02.server-components/01.problem.rsc/ui/index.js b/exercises/02.server-components/01.problem.rsc/ui/index.js new file mode 100644 index 0000000..8ce1d92 --- /dev/null +++ b/exercises/02.server-components/01.problem.rsc/ui/index.js @@ -0,0 +1,50 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +// ๐Ÿ’ฃ App is now an RSC! We don't want to import it here. We'll get it's +// rendered result as data in a fetch request instead. Delete this import: +import { App } from './app.js' +// ๐Ÿ’ฐ you're going to want this: +// import { createFromFetch } from 'react-server-dom-esm/client' +import { shipFallbackSrc } from './img-utils.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +const initialLocation = getGlobalLocation() +// ๐Ÿจ rename initialDataPromise to something more accurate +// (๐Ÿ’ฐ like initialContentFetchPromise) +// ๐Ÿจ also replace /api with /rsc +const initialDataPromise = fetch(`/api${initialLocation}`) + // ๐Ÿ’ฃ we no longer process the response into JSON, instead react-server-dom-esm + // will process it for us. Delete this `then` call: + .then((r) => r.json()) + +// ๐Ÿจ create a variable called initialContentPromise set to createFromFetch(initialContentFetchPromise) + +function Root() { + // ๐Ÿ’ฃ we no longer request data or render the App component, delete these lines: + const { shipId, search, ship, shipResults } = use(initialDataPromise) + return h(App, { shipId, search, ship, shipResults }) + // ๐Ÿจ create a variable called content set to use(initialContentPromise) + // ๐Ÿ’ฏ as a bonus, go ahead and console.log the content variable and check it out in the dev tools! + // ๐Ÿจ return the content +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h(Root), + ), + ), + ) +}) diff --git a/exercises/02.server-components/01.problem.rsc/ui/ship-details.js b/exercises/02.server-components/01.problem.rsc/ui/ship-details.js new file mode 100644 index 0000000..21b4ce1 --- /dev/null +++ b/exercises/02.server-components/01.problem.rsc/ui/ship-details.js @@ -0,0 +1,43 @@ +import { createElement as h } from 'react' +import { getImageUrlForShip } from './img-utils.js' + +export function ShipDetails({ ship }) { + const shipImgSrc = getImageUrlForShip(ship.id, { size: 200 }) + return h( + 'div', + { className: 'ship-info' }, + h( + 'div', + { className: 'ship-info__img-wrapper' }, + h('img', { 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.'), + ), + ) +} diff --git a/exercises/02.server-components/01.problem.rsc/ui/ship-search-results.js b/exercises/02.server-components/01.problem.rsc/ui/ship-search-results.js new file mode 100644 index 0000000..726fbb9 --- /dev/null +++ b/exercises/02.server-components/01.problem.rsc/ui/ship-search-results.js @@ -0,0 +1,29 @@ +import { createElement as h } from 'react' +import { getImageUrlForShip } from './img-utils.js' + +export function SearchResults({ shipId: currentShipId, shipResults, search }) { + return shipResults.ships.map((ship) => { + const href = [ + `/${ship.id}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h( + 'li', + { key: ship.name }, + h( + 'a', + { + href, + style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, + }, + h('img', { + src: getImageUrlForShip(ship.id, { size: 20 }), + alt: ship.name, + }), + ship.name, + ), + ) + }) +} diff --git a/exercises/01.exercises/02.solution.server-context/.gitignore b/exercises/02.server-components/01.solution.rsc/.gitignore similarity index 100% rename from exercises/01.exercises/02.solution.server-context/.gitignore rename to exercises/02.server-components/01.solution.rsc/.gitignore diff --git a/exercises/02.server-components/01.solution.rsc/README.mdx b/exercises/02.server-components/01.solution.rsc/README.mdx new file mode 100644 index 0000000..7208b6b --- /dev/null +++ b/exercises/02.server-components/01.solution.rsc/README.mdx @@ -0,0 +1,13 @@ +# RSCs + + + +๐Ÿ‘จโ€๐Ÿ’ผ Phew, it took a bit of work to get our server environment ready to generate +RSC content, but now that it is, we're no longer sending **any** custom +JavaScript to the client other than what's in +and it will remain that way regardless of how big our app gets until we start +wanting to have interactive bits of UI in our app. + +That's cool. + +But what's even cooler is what we get to do next... diff --git a/exercises/02.server-components/01.solution.rsc/db/ship-api.js b/exercises/02.server-components/01.solution.rsc/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/02.server-components/01.solution.rsc/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/02.server-components/01.solution.rsc/db/ships.json b/exercises/02.server-components/01.solution.rsc/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/02.server-components/01.solution.rsc/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/02.server-components/01.solution.rsc/package.json b/exercises/02.server-components/01.solution.rsc/package.json new file mode 100644 index 0000000..0d40db8 --- /dev/null +++ b/exercises/02.server-components/01.solution.rsc/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__02.server-components__sep__01.solution.rsc", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/02.server-components/01.solution.rsc/public/favicon.ico b/exercises/02.server-components/01.solution.rsc/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/02.server-components/01.solution.rsc/public/favicon.ico differ diff --git a/exercises/02.server-components/01.solution.rsc/public/favicon.svg b/exercises/02.server-components/01.solution.rsc/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/02.server-components/01.solution.rsc/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/02.server-components/01.solution.rsc/public/iframe-sync.js b/exercises/02.server-components/01.solution.rsc/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/02.server-components/01.solution.rsc/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/02.solution.server-context/public/img/broken-ship.webp b/exercises/02.server-components/01.solution.rsc/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/broken-ship.webp rename to exercises/02.server-components/01.solution.rsc/public/img/broken-ship.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/fallback-ship.png b/exercises/02.server-components/01.solution.rsc/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/fallback-ship.png rename to exercises/02.server-components/01.solution.rsc/public/img/fallback-ship.png diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/0268fc4817ad1.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/0268fc4817ad1.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/1ae7b4b92036b.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/1ae7b4b92036b.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/1ff1991efe029.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/1ff1991efe029.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/3ba8aa65ffe6c.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/441f7092a8d44.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/441f7092a8d44.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/5c13d8b28a14a.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/5c13d8b28a14a.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/627c497212456.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/627c497212456.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/670003aed3795.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/670003aed3795.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/6c86fca8b9086.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/6c86fca8b9086.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/6f375578ead88.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/6f375578ead88.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/ab267a5984523.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/ab267a5984523.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/b442531ea32b2.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/b442531ea32b2.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/bc4cbadf89bd3.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/bc4cbadf89bd3.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/cb03cc4e5717e.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/cb03cc4e5717e.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/cfd10fcd2de6c.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/cfd10fcd2de6c.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/d3b8aa65ffe6c.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/d486d48b82b81.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/d486d48b82b81.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/e92cefe4f6727.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/e92cefe4f6727.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/ec7a3f950f99f.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/ec7a3f950f99f.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/f3d9a88e1c234.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/f3d9a88e1c234.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/02.solution.server-context/public/img/ships/fdc13cb488bf1.webp b/exercises/02.server-components/01.solution.rsc/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/img/ships/fdc13cb488bf1.webp rename to exercises/02.server-components/01.solution.rsc/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/02.server-components/01.solution.rsc/public/index.html b/exercises/02.server-components/01.solution.rsc/public/index.html new file mode 100644 index 0000000..1649718 --- /dev/null +++ b/exercises/02.server-components/01.solution.rsc/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/02.solution.server-context/public/style.css b/exercises/02.server-components/01.solution.rsc/public/style.css similarity index 100% rename from exercises/01.exercises/02.solution.server-context/public/style.css rename to exercises/02.server-components/01.solution.rsc/public/style.css diff --git a/exercises/02.server-components/01.solution.rsc/server/app.js b/exercises/02.server-components/01.solution.rsc/server/app.js new file mode 100644 index 0000000..bdf254b --- /dev/null +++ b/exercises/02.server-components/01.solution.rsc/server/app.js @@ -0,0 +1,78 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { getShip, searchShips } from '../db/ship-api.js' +import { App } from '../ui/app.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const ship = shipId ? await getShip({ shipId }) : null + const shipResults = await searchShips({ search }) + const props = { shipId, search, ship, shipResults } + + const { pipe } = renderToPipeableStream(h(App, props)) + pipe(context.env.outgoing) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/02.server-components/01.solution.rsc/tests/playwright.config.js b/exercises/02.server-components/01.solution.rsc/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/02.server-components/01.solution.rsc/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/02.server-components/01.solution.rsc/tests/solution.test.js b/exercises/02.server-components/01.solution.rsc/tests/solution.test.js new file mode 100644 index 0000000..c1ac38f --- /dev/null +++ b/exercises/02.server-components/01.solution.rsc/tests/solution.test.js @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + await filterInput.press('Enter') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + await page.waitForLoadState('networkidle') + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/02.server-components/01.solution.rsc/ui/app.js b/exercises/02.server-components/01.solution.rsc/ui/app.js new file mode 100644 index 0000000..100fa6f --- /dev/null +++ b/exercises/02.server-components/01.solution.rsc/ui/app.js @@ -0,0 +1,37 @@ +import { Fragment, createElement as h } from 'react' +import { ShipDetails } from './ship-details.js' +import { SearchResults } from './ship-search-results.js' + +export function App({ shipId, search, ship, shipResults }) { + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h( + Fragment, + null, + h( + 'form', + {}, + h('input', { + name: 'search', + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + autoFocus: true, + }), + ), + h('ul', null, h(SearchResults, { shipId, search, shipResults })), + ), + ), + h( + 'div', + { className: 'details' }, + shipId + ? h(ShipDetails, { ship }) + : h('p', null, 'Select a ship from the list to see details'), + ), + ) +} diff --git a/exercises/01.exercises/05.solution.bootstrap/src/img-utils.js b/exercises/02.server-components/01.solution.rsc/ui/img-utils.js similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/src/img-utils.js rename to exercises/02.server-components/01.solution.rsc/ui/img-utils.js diff --git a/exercises/02.server-components/01.solution.rsc/ui/index.js b/exercises/02.server-components/01.solution.rsc/ui/index.js new file mode 100644 index 0000000..577ae9e --- /dev/null +++ b/exercises/02.server-components/01.solution.rsc/ui/index.js @@ -0,0 +1,35 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +import { createFromFetch } from 'react-server-dom-esm/client' +import { shipFallbackSrc } from './img-utils.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +const initialLocation = getGlobalLocation() +const initialContentFetchPromise = fetch(`/rsc${initialLocation}`) +const initialContentPromise = createFromFetch(initialContentFetchPromise) + +function Root() { + const content = use(initialContentPromise) + return content +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h(Root), + ), + ), + ) +}) diff --git a/exercises/02.server-components/01.solution.rsc/ui/ship-details.js b/exercises/02.server-components/01.solution.rsc/ui/ship-details.js new file mode 100644 index 0000000..21b4ce1 --- /dev/null +++ b/exercises/02.server-components/01.solution.rsc/ui/ship-details.js @@ -0,0 +1,43 @@ +import { createElement as h } from 'react' +import { getImageUrlForShip } from './img-utils.js' + +export function ShipDetails({ ship }) { + const shipImgSrc = getImageUrlForShip(ship.id, { size: 200 }) + return h( + 'div', + { className: 'ship-info' }, + h( + 'div', + { className: 'ship-info__img-wrapper' }, + h('img', { 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.'), + ), + ) +} diff --git a/exercises/02.server-components/01.solution.rsc/ui/ship-search-results.js b/exercises/02.server-components/01.solution.rsc/ui/ship-search-results.js new file mode 100644 index 0000000..726fbb9 --- /dev/null +++ b/exercises/02.server-components/01.solution.rsc/ui/ship-search-results.js @@ -0,0 +1,29 @@ +import { createElement as h } from 'react' +import { getImageUrlForShip } from './img-utils.js' + +export function SearchResults({ shipId: currentShipId, shipResults, search }) { + return shipResults.ships.map((ship) => { + const href = [ + `/${ship.id}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h( + 'li', + { key: ship.name }, + h( + 'a', + { + href, + style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, + }, + h('img', { + src: getImageUrlForShip(ship.id, { size: 20 }), + alt: ship.name, + }), + ship.name, + ), + ) + }) +} diff --git a/exercises/01.exercises/03.problem.url/.gitignore b/exercises/02.server-components/02.problem.async-components/.gitignore similarity index 100% rename from exercises/01.exercises/03.problem.url/.gitignore rename to exercises/02.server-components/02.problem.async-components/.gitignore diff --git a/exercises/02.server-components/02.problem.async-components/README.mdx b/exercises/02.server-components/02.problem.async-components/README.mdx new file mode 100644 index 0000000..58b374e --- /dev/null +++ b/exercises/02.server-components/02.problem.async-components/README.mdx @@ -0,0 +1,11 @@ +# Async Components + + + +๐Ÿ‘จโ€๐Ÿ’ผ Now that we have RSCs running on our server, our server components can be +`async`! This means we no longer have to worry about loading all of our data +before we render our components and passing that data as props. Instead, we can +have the components load their own data with `async`/`await`. + +So please delete the two `await`s we have in our server code and update our +components to load their own data. diff --git a/exercises/02.server-components/02.problem.async-components/db/ship-api.js b/exercises/02.server-components/02.problem.async-components/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/02.server-components/02.problem.async-components/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/02.server-components/02.problem.async-components/db/ships.json b/exercises/02.server-components/02.problem.async-components/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/02.server-components/02.problem.async-components/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/02.server-components/02.problem.async-components/package.json b/exercises/02.server-components/02.problem.async-components/package.json new file mode 100644 index 0000000..2fdaae9 --- /dev/null +++ b/exercises/02.server-components/02.problem.async-components/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__02.server-components__sep__02.problem.async-components", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/02.server-components/02.problem.async-components/public/favicon.ico b/exercises/02.server-components/02.problem.async-components/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/02.server-components/02.problem.async-components/public/favicon.ico differ diff --git a/exercises/02.server-components/02.problem.async-components/public/favicon.svg b/exercises/02.server-components/02.problem.async-components/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/02.server-components/02.problem.async-components/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/02.server-components/02.problem.async-components/public/iframe-sync.js b/exercises/02.server-components/02.problem.async-components/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/02.server-components/02.problem.async-components/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/03.problem.url/public/img/broken-ship.webp b/exercises/02.server-components/02.problem.async-components/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/broken-ship.webp rename to exercises/02.server-components/02.problem.async-components/public/img/broken-ship.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/fallback-ship.png b/exercises/02.server-components/02.problem.async-components/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/fallback-ship.png rename to exercises/02.server-components/02.problem.async-components/public/img/fallback-ship.png diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/0268fc4817ad1.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/0268fc4817ad1.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/1ae7b4b92036b.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/1ae7b4b92036b.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/1ff1991efe029.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/1ff1991efe029.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/3ba8aa65ffe6c.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/441f7092a8d44.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/441f7092a8d44.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/5c13d8b28a14a.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/5c13d8b28a14a.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/627c497212456.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/627c497212456.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/670003aed3795.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/670003aed3795.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/6c86fca8b9086.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/6c86fca8b9086.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/6f375578ead88.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/6f375578ead88.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/ab267a5984523.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/ab267a5984523.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/b442531ea32b2.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/b442531ea32b2.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/bc4cbadf89bd3.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/bc4cbadf89bd3.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/cb03cc4e5717e.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/cb03cc4e5717e.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/cfd10fcd2de6c.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/cfd10fcd2de6c.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/d3b8aa65ffe6c.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/d486d48b82b81.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/d486d48b82b81.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/e92cefe4f6727.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/e92cefe4f6727.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/ec7a3f950f99f.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/ec7a3f950f99f.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/f3d9a88e1c234.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/f3d9a88e1c234.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/03.problem.url/public/img/ships/fdc13cb488bf1.webp b/exercises/02.server-components/02.problem.async-components/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/03.problem.url/public/img/ships/fdc13cb488bf1.webp rename to exercises/02.server-components/02.problem.async-components/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/02.server-components/02.problem.async-components/public/index.html b/exercises/02.server-components/02.problem.async-components/public/index.html new file mode 100644 index 0000000..1649718 --- /dev/null +++ b/exercises/02.server-components/02.problem.async-components/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/03.problem.url/public/style.css b/exercises/02.server-components/02.problem.async-components/public/style.css similarity index 100% rename from exercises/01.exercises/03.problem.url/public/style.css rename to exercises/02.server-components/02.problem.async-components/public/style.css diff --git a/exercises/02.server-components/02.problem.async-components/server/app.js b/exercises/02.server-components/02.problem.async-components/server/app.js new file mode 100644 index 0000000..2dc9a8f --- /dev/null +++ b/exercises/02.server-components/02.problem.async-components/server/app.js @@ -0,0 +1,79 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { getShip, searchShips } from '../db/ship-api.js' +import { App } from '../ui/app.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + // ๐Ÿ’ฃ delete the ship and shipResults + const ship = shipId ? await getShip({ shipId }) : null + const shipResults = await searchShips({ search }) + // ๐Ÿ’ฃ remove them from the props object too + const props = { shipId, search, ship, shipResults } + const { pipe } = renderToPipeableStream(h(App, props)) + pipe(context.env.outgoing) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/02.server-components/02.problem.async-components/tests/playwright.config.js b/exercises/02.server-components/02.problem.async-components/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/02.server-components/02.problem.async-components/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/02.server-components/02.problem.async-components/tests/solution.test.js b/exercises/02.server-components/02.problem.async-components/tests/solution.test.js new file mode 100644 index 0000000..c1ac38f --- /dev/null +++ b/exercises/02.server-components/02.problem.async-components/tests/solution.test.js @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + await filterInput.press('Enter') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + await page.waitForLoadState('networkidle') + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/02.server-components/02.problem.async-components/ui/app.js b/exercises/02.server-components/02.problem.async-components/ui/app.js new file mode 100644 index 0000000..c9d0ffe --- /dev/null +++ b/exercises/02.server-components/02.problem.async-components/ui/app.js @@ -0,0 +1,40 @@ +import { Fragment, createElement as h } from 'react' +import { ShipDetails } from './ship-details.js' +import { SearchResults } from './ship-search-results.js' + +// ๐Ÿ’ฃ remove the ship and shipResults props +export function App({ shipId, search, ship, shipResults }) { + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h( + Fragment, + null, + h( + 'form', + {}, + h('input', { + name: 'search', + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + autoFocus: true, + }), + ), + // ๐Ÿ’ฃ remove the shipResults prop + h('ul', null, h(SearchResults, { shipId, search, shipResults })), + ), + ), + h( + 'div', + { className: 'details' }, + shipId + ? // ๐Ÿจ replace the ship prop with a shipId prop + h(ShipDetails, { ship }) + : h('p', null, 'Select a ship from the list to see details'), + ), + ) +} diff --git a/exercises/01.exercises/06.problem.import-map/src/img-utils.js b/exercises/02.server-components/02.problem.async-components/ui/img-utils.js similarity index 100% rename from exercises/01.exercises/06.problem.import-map/src/img-utils.js rename to exercises/02.server-components/02.problem.async-components/ui/img-utils.js diff --git a/exercises/02.server-components/02.problem.async-components/ui/index.js b/exercises/02.server-components/02.problem.async-components/ui/index.js new file mode 100644 index 0000000..577ae9e --- /dev/null +++ b/exercises/02.server-components/02.problem.async-components/ui/index.js @@ -0,0 +1,35 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +import { createFromFetch } from 'react-server-dom-esm/client' +import { shipFallbackSrc } from './img-utils.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +const initialLocation = getGlobalLocation() +const initialContentFetchPromise = fetch(`/rsc${initialLocation}`) +const initialContentPromise = createFromFetch(initialContentFetchPromise) + +function Root() { + const content = use(initialContentPromise) + return content +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h(Root), + ), + ), + ) +}) diff --git a/exercises/02.server-components/02.problem.async-components/ui/ship-details.js b/exercises/02.server-components/02.problem.async-components/ui/ship-details.js new file mode 100644 index 0000000..a78c9ce --- /dev/null +++ b/exercises/02.server-components/02.problem.async-components/ui/ship-details.js @@ -0,0 +1,48 @@ +import { createElement as h } from 'react' +// ๐Ÿ’ฐ you're gonna need this: +// import { getShip } from '../db/ship-api.js' +import { getImageUrlForShip } from './img-utils.js' + +// ๐Ÿจ replace the ship prop with a shipId prop +export function ShipDetails({ ship }) { + // ๐Ÿจ get the ship using getShip({ shipId }) + // ๐Ÿ’ฐ you can use async/await! + const shipImgSrc = getImageUrlForShip(ship.id, { size: 200 }) + return h( + 'div', + { className: 'ship-info' }, + h( + 'div', + { className: 'ship-info__img-wrapper' }, + h('img', { 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.'), + ), + ) +} diff --git a/exercises/02.server-components/02.problem.async-components/ui/ship-search-results.js b/exercises/02.server-components/02.problem.async-components/ui/ship-search-results.js new file mode 100644 index 0000000..9abcd29 --- /dev/null +++ b/exercises/02.server-components/02.problem.async-components/ui/ship-search-results.js @@ -0,0 +1,34 @@ +import { createElement as h } from 'react' +// ๐Ÿ’ฐ you're gonna need this +// import { searchShips } from '../db/ship-api.js' +import { getImageUrlForShip } from './img-utils.js' + +// ๐Ÿ’ฃ remove the shipResults prop +export function SearchResults({ shipId: currentShipId, shipResults, search }) { + // ๐Ÿจ get the shipResults from searchShips({ search }) + // ๐Ÿ’ฐ you can use async/await! + return shipResults.ships.map((ship) => { + const href = [ + `/${ship.id}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h( + 'li', + { key: ship.name }, + h( + 'a', + { + href, + style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, + }, + h('img', { + src: getImageUrlForShip(ship.id, { size: 20 }), + alt: ship.name, + }), + ship.name, + ), + ) + }) +} diff --git a/exercises/01.exercises/03.solution.url/.gitignore b/exercises/02.server-components/02.solution.async-components/.gitignore similarity index 100% rename from exercises/01.exercises/03.solution.url/.gitignore rename to exercises/02.server-components/02.solution.async-components/.gitignore diff --git a/exercises/02.server-components/02.solution.async-components/README.mdx b/exercises/02.server-components/02.solution.async-components/README.mdx new file mode 100644 index 0000000..09c72e3 --- /dev/null +++ b/exercises/02.server-components/02.solution.async-components/README.mdx @@ -0,0 +1,8 @@ +# Async Components + + + +๐Ÿ‘จโ€๐Ÿ’ผ Isn't that great!? Now our components are encapsulating so much about +themselves. Composition FOR THE WIN! ๐ŸŽ‰ On top of that, before these components +were in the browser and didn't have access to the database directly. Now we have +access to anything on the server which makes this even more powerful. diff --git a/exercises/02.server-components/02.solution.async-components/db/ship-api.js b/exercises/02.server-components/02.solution.async-components/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/02.server-components/02.solution.async-components/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/02.server-components/02.solution.async-components/db/ships.json b/exercises/02.server-components/02.solution.async-components/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/02.server-components/02.solution.async-components/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/02.server-components/02.solution.async-components/package.json b/exercises/02.server-components/02.solution.async-components/package.json new file mode 100644 index 0000000..7a45bfe --- /dev/null +++ b/exercises/02.server-components/02.solution.async-components/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__02.server-components__sep__02.solution.async-components", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/02.server-components/02.solution.async-components/public/favicon.ico b/exercises/02.server-components/02.solution.async-components/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/02.server-components/02.solution.async-components/public/favicon.ico differ diff --git a/exercises/02.server-components/02.solution.async-components/public/favicon.svg b/exercises/02.server-components/02.solution.async-components/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/02.server-components/02.solution.async-components/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/02.server-components/02.solution.async-components/public/iframe-sync.js b/exercises/02.server-components/02.solution.async-components/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/02.server-components/02.solution.async-components/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/03.solution.url/public/img/broken-ship.webp b/exercises/02.server-components/02.solution.async-components/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/broken-ship.webp rename to exercises/02.server-components/02.solution.async-components/public/img/broken-ship.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/fallback-ship.png b/exercises/02.server-components/02.solution.async-components/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/fallback-ship.png rename to exercises/02.server-components/02.solution.async-components/public/img/fallback-ship.png diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/0268fc4817ad1.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/0268fc4817ad1.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/1ae7b4b92036b.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/1ae7b4b92036b.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/1ff1991efe029.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/1ff1991efe029.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/3ba8aa65ffe6c.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/441f7092a8d44.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/441f7092a8d44.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/5c13d8b28a14a.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/5c13d8b28a14a.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/627c497212456.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/627c497212456.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/670003aed3795.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/670003aed3795.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/6c86fca8b9086.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/6c86fca8b9086.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/6f375578ead88.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/6f375578ead88.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/ab267a5984523.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/ab267a5984523.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/b442531ea32b2.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/b442531ea32b2.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/bc4cbadf89bd3.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/bc4cbadf89bd3.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/cb03cc4e5717e.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/cb03cc4e5717e.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/cfd10fcd2de6c.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/cfd10fcd2de6c.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/d3b8aa65ffe6c.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/d486d48b82b81.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/d486d48b82b81.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/e92cefe4f6727.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/e92cefe4f6727.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/ec7a3f950f99f.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/ec7a3f950f99f.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/f3d9a88e1c234.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/f3d9a88e1c234.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/03.solution.url/public/img/ships/fdc13cb488bf1.webp b/exercises/02.server-components/02.solution.async-components/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/03.solution.url/public/img/ships/fdc13cb488bf1.webp rename to exercises/02.server-components/02.solution.async-components/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/02.server-components/02.solution.async-components/public/index.html b/exercises/02.server-components/02.solution.async-components/public/index.html new file mode 100644 index 0000000..1649718 --- /dev/null +++ b/exercises/02.server-components/02.solution.async-components/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/03.solution.url/public/style.css b/exercises/02.server-components/02.solution.async-components/public/style.css similarity index 100% rename from exercises/01.exercises/03.solution.url/public/style.css rename to exercises/02.server-components/02.solution.async-components/public/style.css diff --git a/exercises/02.server-components/02.solution.async-components/server/app.js b/exercises/02.server-components/02.solution.async-components/server/app.js new file mode 100644 index 0000000..a86aa57 --- /dev/null +++ b/exercises/02.server-components/02.solution.async-components/server/app.js @@ -0,0 +1,74 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const props = { shipId, search } + const { pipe } = renderToPipeableStream(h(App, props)) + pipe(context.env.outgoing) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/02.server-components/02.solution.async-components/tests/playwright.config.js b/exercises/02.server-components/02.solution.async-components/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/02.server-components/02.solution.async-components/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/02.server-components/02.solution.async-components/tests/solution.test.js b/exercises/02.server-components/02.solution.async-components/tests/solution.test.js new file mode 100644 index 0000000..c1ac38f --- /dev/null +++ b/exercises/02.server-components/02.solution.async-components/tests/solution.test.js @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + await filterInput.press('Enter') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + await page.waitForLoadState('networkidle') + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/02.server-components/02.solution.async-components/ui/app.js b/exercises/02.server-components/02.solution.async-components/ui/app.js new file mode 100644 index 0000000..d4ecfde --- /dev/null +++ b/exercises/02.server-components/02.solution.async-components/ui/app.js @@ -0,0 +1,37 @@ +import { Fragment, createElement as h } from 'react' +import { ShipDetails } from './ship-details.js' +import { SearchResults } from './ship-search-results.js' + +export function App({ shipId, search }) { + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h( + Fragment, + null, + h( + 'form', + {}, + h('input', { + name: 'search', + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + autoFocus: true, + }), + ), + h('ul', null, h(SearchResults, { shipId, search })), + ), + ), + h( + 'div', + { className: 'details' }, + shipId + ? h(ShipDetails, { shipId }) + : h('p', null, 'Select a ship from the list to see details'), + ), + ) +} diff --git a/exercises/01.exercises/06.solution.import-map/src/img-utils.js b/exercises/02.server-components/02.solution.async-components/ui/img-utils.js similarity index 100% rename from exercises/01.exercises/06.solution.import-map/src/img-utils.js rename to exercises/02.server-components/02.solution.async-components/ui/img-utils.js diff --git a/exercises/02.server-components/02.solution.async-components/ui/index.js b/exercises/02.server-components/02.solution.async-components/ui/index.js new file mode 100644 index 0000000..577ae9e --- /dev/null +++ b/exercises/02.server-components/02.solution.async-components/ui/index.js @@ -0,0 +1,35 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +import { createFromFetch } from 'react-server-dom-esm/client' +import { shipFallbackSrc } from './img-utils.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +const initialLocation = getGlobalLocation() +const initialContentFetchPromise = fetch(`/rsc${initialLocation}`) +const initialContentPromise = createFromFetch(initialContentFetchPromise) + +function Root() { + const content = use(initialContentPromise) + return content +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h(Root), + ), + ), + ) +}) diff --git a/exercises/02.server-components/02.solution.async-components/ui/ship-details.js b/exercises/02.server-components/02.solution.async-components/ui/ship-details.js new file mode 100644 index 0000000..fd3d0eb --- /dev/null +++ b/exercises/02.server-components/02.solution.async-components/ui/ship-details.js @@ -0,0 +1,45 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { getImageUrlForShip } from './img-utils.js' + +export async function ShipDetails({ shipId }) { + 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('img', { 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.'), + ), + ) +} diff --git a/exercises/02.server-components/02.solution.async-components/ui/ship-search-results.js b/exercises/02.server-components/02.solution.async-components/ui/ship-search-results.js new file mode 100644 index 0000000..be4ece7 --- /dev/null +++ b/exercises/02.server-components/02.solution.async-components/ui/ship-search-results.js @@ -0,0 +1,31 @@ +import { createElement as h } from 'react' +import { searchShips } from '../db/ship-api.js' +import { getImageUrlForShip } from './img-utils.js' + +export async function SearchResults({ shipId: currentShipId, search }) { + const shipResults = await searchShips({ search }) + return shipResults.ships.map((ship) => { + const href = [ + `/${ship.id}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h( + 'li', + { key: ship.name }, + h( + 'a', + { + href, + style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, + }, + h('img', { + src: getImageUrlForShip(ship.id, { size: 20 }), + alt: ship.name, + }), + ship.name, + ), + ) + }) +} diff --git a/exercises/01.exercises/04.problem.async-components/.gitignore b/exercises/02.server-components/03.problem.streaming/.gitignore similarity index 100% rename from exercises/01.exercises/04.problem.async-components/.gitignore rename to exercises/02.server-components/03.problem.streaming/.gitignore diff --git a/exercises/02.server-components/03.problem.streaming/README.mdx b/exercises/02.server-components/03.problem.streaming/README.mdx new file mode 100644 index 0000000..df85a07 --- /dev/null +++ b/exercises/02.server-components/03.problem.streaming/README.mdx @@ -0,0 +1,25 @@ +# Streaming + + + +๐Ÿงโ€โ™‚๏ธ I've added a long delay to the `searchShips` API to make the improvements +here more noticable. Check to see +how. + +๐Ÿ‘จโ€๐Ÿ’ผ Right now we display our loading screen until all the components finish +rendering. But if we want to put some well placed `Suspense` boundaries around +different server components, we can have a more granular loading experience. + +Please add some suspense boundaries around the search results and ship details +so we can have things load in as they become ready. + +As you do, I want you to appreciate the out-of-order streaming we get here which +enables a lot of flexibility in how we load things in (allowing us to prioritize +the most important parts of the page first). + + + Note: more spinners is not necessarily better. In fact, it would be better to + just server-render all of this stuff from the start and make it really fast so + you don't need loading states at all and you avoid cumulative layout shift. + But we're sticking with client-side rendering for now to keep things simple. + diff --git a/exercises/02.server-components/03.problem.streaming/db/ship-api.js b/exercises/02.server-components/03.problem.streaming/db/ship-api.js new file mode 100644 index 0000000..163427c --- /dev/null +++ b/exercises/02.server-components/03.problem.streaming/db/ship-api.js @@ -0,0 +1,61 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + // ๐Ÿงโ€โ™‚๏ธ I've added this for you so you can test things out for this exercise + delay = 4000 + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/02.server-components/03.problem.streaming/db/ships.json b/exercises/02.server-components/03.problem.streaming/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/02.server-components/03.problem.streaming/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/02.server-components/03.problem.streaming/package.json b/exercises/02.server-components/03.problem.streaming/package.json new file mode 100644 index 0000000..d199e50 --- /dev/null +++ b/exercises/02.server-components/03.problem.streaming/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__02.server-components__sep__03.problem.streaming", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/02.server-components/03.problem.streaming/public/favicon.ico b/exercises/02.server-components/03.problem.streaming/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/02.server-components/03.problem.streaming/public/favicon.ico differ diff --git a/exercises/02.server-components/03.problem.streaming/public/favicon.svg b/exercises/02.server-components/03.problem.streaming/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/02.server-components/03.problem.streaming/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/02.server-components/03.problem.streaming/public/iframe-sync.js b/exercises/02.server-components/03.problem.streaming/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/02.server-components/03.problem.streaming/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/04.problem.async-components/public/img/broken-ship.webp b/exercises/02.server-components/03.problem.streaming/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/broken-ship.webp rename to exercises/02.server-components/03.problem.streaming/public/img/broken-ship.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/fallback-ship.png b/exercises/02.server-components/03.problem.streaming/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/fallback-ship.png rename to exercises/02.server-components/03.problem.streaming/public/img/fallback-ship.png diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/0268fc4817ad1.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/0268fc4817ad1.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/1ae7b4b92036b.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/1ae7b4b92036b.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/1ff1991efe029.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/1ff1991efe029.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/3ba8aa65ffe6c.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/441f7092a8d44.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/441f7092a8d44.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/5c13d8b28a14a.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/5c13d8b28a14a.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/627c497212456.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/627c497212456.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/670003aed3795.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/670003aed3795.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/6c86fca8b9086.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/6c86fca8b9086.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/6f375578ead88.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/6f375578ead88.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/ab267a5984523.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/ab267a5984523.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/b442531ea32b2.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/b442531ea32b2.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/bc4cbadf89bd3.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/bc4cbadf89bd3.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/cb03cc4e5717e.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/cb03cc4e5717e.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/cfd10fcd2de6c.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/cfd10fcd2de6c.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/d3b8aa65ffe6c.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/d486d48b82b81.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/d486d48b82b81.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/e92cefe4f6727.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/e92cefe4f6727.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/ec7a3f950f99f.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/ec7a3f950f99f.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/f3d9a88e1c234.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/f3d9a88e1c234.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/04.problem.async-components/public/img/ships/fdc13cb488bf1.webp b/exercises/02.server-components/03.problem.streaming/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/img/ships/fdc13cb488bf1.webp rename to exercises/02.server-components/03.problem.streaming/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/02.server-components/03.problem.streaming/public/index.html b/exercises/02.server-components/03.problem.streaming/public/index.html new file mode 100644 index 0000000..1649718 --- /dev/null +++ b/exercises/02.server-components/03.problem.streaming/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/04.problem.async-components/public/style.css b/exercises/02.server-components/03.problem.streaming/public/style.css similarity index 100% rename from exercises/01.exercises/04.problem.async-components/public/style.css rename to exercises/02.server-components/03.problem.streaming/public/style.css diff --git a/exercises/02.server-components/03.problem.streaming/server/app.js b/exercises/02.server-components/03.problem.streaming/server/app.js new file mode 100644 index 0000000..a86aa57 --- /dev/null +++ b/exercises/02.server-components/03.problem.streaming/server/app.js @@ -0,0 +1,74 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const props = { shipId, search } + const { pipe } = renderToPipeableStream(h(App, props)) + pipe(context.env.outgoing) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/02.server-components/03.problem.streaming/tests/playwright.config.js b/exercises/02.server-components/03.problem.streaming/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/02.server-components/03.problem.streaming/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/02.server-components/03.problem.streaming/tests/solution.test.js b/exercises/02.server-components/03.problem.streaming/tests/solution.test.js new file mode 100644 index 0000000..4fdd0f7 --- /dev/null +++ b/exercises/02.server-components/03.problem.streaming/tests/solution.test.js @@ -0,0 +1,58 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Wait for the loading placeholders to disappear + await page.waitForSelector('li a:has-text("... loading")', { + state: 'detached', + }) + + // Verify that the list is populated with actual ship names + const shipList = page.getByRole('list').first() + await expect(shipList.getByRole('link').first()).not.toHaveText('... loading') + + // Perform a search + await filterInput.fill('hopper') + await filterInput.press('Enter') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + // Check for loading indicators after search + await expect( + page.locator('li a:has-text("... loading")').first(), + ).toBeVisible() + // Wait for the loading placeholders to disappear + await page.waitForSelector('li a:has-text("... loading")', { + state: 'detached', + }) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/02.server-components/03.problem.streaming/ui/app.js b/exercises/02.server-components/03.problem.streaming/ui/app.js new file mode 100644 index 0000000..eb0d708 --- /dev/null +++ b/exercises/02.server-components/03.problem.streaming/ui/app.js @@ -0,0 +1,57 @@ +// ๐Ÿจ you'll want to import Suspense from react +import { /* Suspense, */ Fragment, createElement as h } from 'react' +import { + ShipDetails, + // ๐Ÿ’ฐ you'll want this: + // ShipFallback +} from './ship-details.js' +import { + SearchResults, + // ๐Ÿ’ฐ you'll want this: + // SearchResultsFallback +} from './ship-search-results.js' + +export function App({ shipId, search }) { + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h( + Fragment, + null, + h( + 'form', + {}, + h('input', { + name: 'search', + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + autoFocus: true, + }), + ), + h( + 'ul', + null, + // ๐Ÿจ wrap this in a Suspense boundary with the fallback set to + // h(SearchResultsFallback) + // ๐Ÿ’ฐ remember it's h(Component, props, child1, child2, child3) + // ๐Ÿ’ฐ don't feel too bad if you need to reference the diff on this one + // it's kinda hard to go back to non-JSX after you've been used to + // using JSX for a while ๐Ÿ˜… + h(SearchResults, { shipId, search }), + ), + ), + ), + h( + 'div', + { className: 'details' }, + shipId + ? // ๐Ÿจ wrap this in a Suspense boundary with the fallback set to h(ShipFallback, { shipId }) + h(ShipDetails, { shipId }) + : h('p', null, 'Select a ship from the list to see details'), + ), + ) +} diff --git a/exercises/01.exercises/07.problem.module-graph/src/img-utils.js b/exercises/02.server-components/03.problem.streaming/ui/img-utils.js similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/src/img-utils.js rename to exercises/02.server-components/03.problem.streaming/ui/img-utils.js diff --git a/exercises/02.server-components/03.problem.streaming/ui/index.js b/exercises/02.server-components/03.problem.streaming/ui/index.js new file mode 100644 index 0000000..577ae9e --- /dev/null +++ b/exercises/02.server-components/03.problem.streaming/ui/index.js @@ -0,0 +1,35 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +import { createFromFetch } from 'react-server-dom-esm/client' +import { shipFallbackSrc } from './img-utils.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +const initialLocation = getGlobalLocation() +const initialContentFetchPromise = fetch(`/rsc${initialLocation}`) +const initialContentPromise = createFromFetch(initialContentFetchPromise) + +function Root() { + const content = use(initialContentPromise) + return content +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h(Root), + ), + ), + ) +}) diff --git a/exercises/02.server-components/03.problem.streaming/ui/ship-details.js b/exercises/02.server-components/03.problem.streaming/ui/ship-details.js new file mode 100644 index 0000000..28a2ad0 --- /dev/null +++ b/exercises/02.server-components/03.problem.streaming/ui/ship-details.js @@ -0,0 +1,81 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { getImageUrlForShip } from './img-utils.js' + +export async function ShipDetails({ shipId }) { + 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('img', { 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({ shipId }) { + return h( + 'div', + { className: 'ship-info' }, + h( + 'div', + { className: 'ship-info__img-wrapper' }, + h('img', { + 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)')), + ), + ), + ), + ), + ) +} diff --git a/exercises/02.server-components/03.problem.streaming/ui/ship-search-results.js b/exercises/02.server-components/03.problem.streaming/ui/ship-search-results.js new file mode 100644 index 0000000..961dd0e --- /dev/null +++ b/exercises/02.server-components/03.problem.streaming/ui/ship-search-results.js @@ -0,0 +1,48 @@ +import { createElement as h } from 'react' +import { searchShips } from '../db/ship-api.js' +import { getImageUrlForShip, shipFallbackSrc } from './img-utils.js' + +export async function SearchResults({ shipId: currentShipId, search }) { + const shipResults = await searchShips({ search }) + return shipResults.ships.map((ship) => { + const href = [ + `/${ship.id}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h( + 'li', + { key: ship.name }, + h( + 'a', + { + href, + style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, + }, + h('img', { + 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/01.exercises/04.solution.async-components/.gitignore b/exercises/02.server-components/03.solution.streaming/.gitignore similarity index 100% rename from exercises/01.exercises/04.solution.async-components/.gitignore rename to exercises/02.server-components/03.solution.streaming/.gitignore diff --git a/exercises/02.server-components/03.solution.streaming/README.mdx b/exercises/02.server-components/03.solution.streaming/README.mdx new file mode 100644 index 0000000..0bad5e9 --- /dev/null +++ b/exercises/02.server-components/03.solution.streaming/README.mdx @@ -0,0 +1,7 @@ +# Streaming + + + +๐Ÿ‘จโ€๐Ÿ’ผ Great! Now we're able to load the shell of application and things will stream +in as they're ready. Again, this could be improved with server-side rendering, +but for now we're in a better state than before! diff --git a/exercises/02.server-components/03.solution.streaming/db/ship-api.js b/exercises/02.server-components/03.solution.streaming/db/ship-api.js new file mode 100644 index 0000000..163427c --- /dev/null +++ b/exercises/02.server-components/03.solution.streaming/db/ship-api.js @@ -0,0 +1,61 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + // ๐Ÿงโ€โ™‚๏ธ I've added this for you so you can test things out for this exercise + delay = 4000 + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/02.server-components/03.solution.streaming/db/ships.json b/exercises/02.server-components/03.solution.streaming/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/02.server-components/03.solution.streaming/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/02.server-components/03.solution.streaming/package.json b/exercises/02.server-components/03.solution.streaming/package.json new file mode 100644 index 0000000..4154d20 --- /dev/null +++ b/exercises/02.server-components/03.solution.streaming/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__02.server-components__sep__03.solution.streaming", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/02.server-components/03.solution.streaming/public/favicon.ico b/exercises/02.server-components/03.solution.streaming/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/02.server-components/03.solution.streaming/public/favicon.ico differ diff --git a/exercises/02.server-components/03.solution.streaming/public/favicon.svg b/exercises/02.server-components/03.solution.streaming/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/02.server-components/03.solution.streaming/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/02.server-components/03.solution.streaming/public/iframe-sync.js b/exercises/02.server-components/03.solution.streaming/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/02.server-components/03.solution.streaming/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/04.solution.async-components/public/img/broken-ship.webp b/exercises/02.server-components/03.solution.streaming/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/broken-ship.webp rename to exercises/02.server-components/03.solution.streaming/public/img/broken-ship.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/fallback-ship.png b/exercises/02.server-components/03.solution.streaming/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/fallback-ship.png rename to exercises/02.server-components/03.solution.streaming/public/img/fallback-ship.png diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/0268fc4817ad1.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/0268fc4817ad1.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/1ae7b4b92036b.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/1ae7b4b92036b.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/1ff1991efe029.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/1ff1991efe029.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/3ba8aa65ffe6c.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/441f7092a8d44.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/441f7092a8d44.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/5c13d8b28a14a.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/5c13d8b28a14a.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/627c497212456.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/627c497212456.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/670003aed3795.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/670003aed3795.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/6c86fca8b9086.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/6c86fca8b9086.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/6f375578ead88.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/6f375578ead88.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/ab267a5984523.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/ab267a5984523.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/b442531ea32b2.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/b442531ea32b2.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/bc4cbadf89bd3.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/bc4cbadf89bd3.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/cb03cc4e5717e.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/cb03cc4e5717e.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/cfd10fcd2de6c.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/cfd10fcd2de6c.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/d3b8aa65ffe6c.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/d486d48b82b81.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/d486d48b82b81.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/e92cefe4f6727.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/e92cefe4f6727.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/ec7a3f950f99f.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/ec7a3f950f99f.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/f3d9a88e1c234.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/f3d9a88e1c234.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/04.solution.async-components/public/img/ships/fdc13cb488bf1.webp b/exercises/02.server-components/03.solution.streaming/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/img/ships/fdc13cb488bf1.webp rename to exercises/02.server-components/03.solution.streaming/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/02.server-components/03.solution.streaming/public/index.html b/exercises/02.server-components/03.solution.streaming/public/index.html new file mode 100644 index 0000000..1649718 --- /dev/null +++ b/exercises/02.server-components/03.solution.streaming/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/04.solution.async-components/public/style.css b/exercises/02.server-components/03.solution.streaming/public/style.css similarity index 100% rename from exercises/01.exercises/04.solution.async-components/public/style.css rename to exercises/02.server-components/03.solution.streaming/public/style.css diff --git a/exercises/02.server-components/03.solution.streaming/server/app.js b/exercises/02.server-components/03.solution.streaming/server/app.js new file mode 100644 index 0000000..a86aa57 --- /dev/null +++ b/exercises/02.server-components/03.solution.streaming/server/app.js @@ -0,0 +1,74 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const props = { shipId, search } + const { pipe } = renderToPipeableStream(h(App, props)) + pipe(context.env.outgoing) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/02.server-components/03.solution.streaming/tests/playwright.config.js b/exercises/02.server-components/03.solution.streaming/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/02.server-components/03.solution.streaming/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/02.server-components/03.solution.streaming/tests/streaming.test.js b/exercises/02.server-components/03.solution.streaming/tests/streaming.test.js new file mode 100644 index 0000000..416fc27 --- /dev/null +++ b/exercises/02.server-components/03.solution.streaming/tests/streaming.test.js @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + test.setTimeout(20000) + + await page.goto('/') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Wait for the loading placeholders to disappear + await page.waitForSelector('li a:has-text("... loading")', { + state: 'detached', + }) + + await page.waitForLoadState('networkidle') + + // Perform a search + await filterInput.fill('hopper') + await filterInput.press('Enter') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + // Check for loading indicators after search + await expect( + page.locator('li a:has-text("... loading")').first(), + ).toBeVisible() + + await page.waitForLoadState('networkidle') + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + await page.waitForLoadState('networkidle') + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/02.server-components/03.solution.streaming/ui/app.js b/exercises/02.server-components/03.solution.streaming/ui/app.js new file mode 100644 index 0000000..9ad3862 --- /dev/null +++ b/exercises/02.server-components/03.solution.streaming/ui/app.js @@ -0,0 +1,49 @@ +import { Fragment, Suspense, createElement as h } from 'react' +import { ShipDetails, ShipFallback } from './ship-details.js' +import { SearchResults, SearchResultsFallback } from './ship-search-results.js' + +export function App({ shipId, search }) { + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h( + Fragment, + null, + h( + 'form', + {}, + h('input', { + name: 'search', + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + autoFocus: true, + }), + ), + h( + 'ul', + null, + h( + Suspense, + { fallback: h(SearchResultsFallback) }, + h(SearchResults, { shipId, search }), + ), + ), + ), + ), + h( + 'div', + { className: 'details' }, + shipId + ? h( + Suspense, + { fallback: h(ShipFallback, { shipId }) }, + h(ShipDetails, { shipId }), + ) + : h('p', null, 'Select a ship from the list to see details'), + ), + ) +} diff --git a/exercises/01.exercises/07.solution.module-graph/src/img-utils.js b/exercises/02.server-components/03.solution.streaming/ui/img-utils.js similarity index 100% rename from exercises/01.exercises/07.solution.module-graph/src/img-utils.js rename to exercises/02.server-components/03.solution.streaming/ui/img-utils.js diff --git a/exercises/02.server-components/03.solution.streaming/ui/index.js b/exercises/02.server-components/03.solution.streaming/ui/index.js new file mode 100644 index 0000000..577ae9e --- /dev/null +++ b/exercises/02.server-components/03.solution.streaming/ui/index.js @@ -0,0 +1,35 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +import { createFromFetch } from 'react-server-dom-esm/client' +import { shipFallbackSrc } from './img-utils.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +const initialLocation = getGlobalLocation() +const initialContentFetchPromise = fetch(`/rsc${initialLocation}`) +const initialContentPromise = createFromFetch(initialContentFetchPromise) + +function Root() { + const content = use(initialContentPromise) + return content +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h(Root), + ), + ), + ) +}) diff --git a/exercises/02.server-components/03.solution.streaming/ui/ship-details.js b/exercises/02.server-components/03.solution.streaming/ui/ship-details.js new file mode 100644 index 0000000..28a2ad0 --- /dev/null +++ b/exercises/02.server-components/03.solution.streaming/ui/ship-details.js @@ -0,0 +1,81 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { getImageUrlForShip } from './img-utils.js' + +export async function ShipDetails({ shipId }) { + 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('img', { 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({ shipId }) { + return h( + 'div', + { className: 'ship-info' }, + h( + 'div', + { className: 'ship-info__img-wrapper' }, + h('img', { + 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)')), + ), + ), + ), + ), + ) +} diff --git a/exercises/02.server-components/03.solution.streaming/ui/ship-search-results.js b/exercises/02.server-components/03.solution.streaming/ui/ship-search-results.js new file mode 100644 index 0000000..961dd0e --- /dev/null +++ b/exercises/02.server-components/03.solution.streaming/ui/ship-search-results.js @@ -0,0 +1,48 @@ +import { createElement as h } from 'react' +import { searchShips } from '../db/ship-api.js' +import { getImageUrlForShip, shipFallbackSrc } from './img-utils.js' + +export async function SearchResults({ shipId: currentShipId, search }) { + const shipResults = await searchShips({ search }) + return shipResults.ships.map((ship) => { + const href = [ + `/${ship.id}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h( + 'li', + { key: ship.name }, + h( + 'a', + { + href, + style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, + }, + h('img', { + 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/01.exercises/05.problem.bootstrap/.gitignore b/exercises/02.server-components/04.problem.server-context/.gitignore similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/.gitignore rename to exercises/02.server-components/04.problem.server-context/.gitignore diff --git a/exercises/02.server-components/04.problem.server-context/README.mdx b/exercises/02.server-components/04.problem.server-context/README.mdx new file mode 100644 index 0000000..1c5e4a4 --- /dev/null +++ b/exercises/02.server-components/04.problem.server-context/README.mdx @@ -0,0 +1,87 @@ +# Server Context + + + +๐Ÿ‘จโ€๐Ÿ’ผ It's kind of annoying to have to send the `search` and `shipId` props down +through the `App` to all our components below. It would be nice if we could +use context to share these values... + +๐Ÿฆ‰ One of the limitations of React Server Components is the lack of support for +React Context. + +React Server Components can absolutely render components that use context: + +```jsx +async function MyServerComponent() { + return ( +
+ + Pet Chooser + + +
+ ) +} +// assume MyCombobox is a client component that uses context +``` + +But they can't `use(Context)` themselves. + +This is a bummer because another benefit of context is avoiding prop drilling +for things like the user object, theme, or localization. + +Luckily for us, Node.js has an answer. Again, this isn't a Node.js workshop, +but this is a common problem that you'll experience when using RSCs and this +is the solution recommended by the React team. + +The answer is [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html). +Here's a simple example from the docs: + +```js +import { AsyncLocalStorage } from 'node:async_hooks' + +const userStorage = new AsyncLocalStorage() + +// somewhere later in your code +const user = await getUser() +try { + userStorage.run(user, () => { + logUser() // Returns the user object + setTimeout(() => { + logUser() // Logs the user object + }, 200) + throw new Error() + }) +} catch (e) { + logUser() // Logs undefined +} + +function logUser() { + const user = userStorage.getStore() + console.log({ user }) +} +``` + +It's a little magical, but the idea is that you can store a value in the +`AsyncLocalStorage` and then access it from anywhere in your code, so long as +the async operation was created within the `run` callback. + +๐Ÿ‘จโ€๐Ÿ’ผ Great, thanks Olivia! So what I want you to do is use this to make the +`search` and `shipId` values available to all components in the tree without +having to pass them down as props. + +๐Ÿจ First you'll want to create a module to create our async storage object. So +let's start by creating and stick +this in there: + +```js +import { AsyncLocalStorage } from 'node:async_hooks' + +export const shipDataStorage = new AsyncLocalStorage() +``` + +From there, you can import it in to provide +the `search` and `shipId` to the rendered components, remove all the prop +drilling from and access the values necessary +from the async storage in +and . diff --git a/exercises/02.server-components/04.problem.server-context/db/ship-api.js b/exercises/02.server-components/04.problem.server-context/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/02.server-components/04.problem.server-context/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/02.server-components/04.problem.server-context/db/ships.json b/exercises/02.server-components/04.problem.server-context/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/02.server-components/04.problem.server-context/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/02.server-components/04.problem.server-context/package.json b/exercises/02.server-components/04.problem.server-context/package.json new file mode 100644 index 0000000..b0c318a --- /dev/null +++ b/exercises/02.server-components/04.problem.server-context/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__02.server-components__sep__04.problem.server-context", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/02.server-components/04.problem.server-context/public/favicon.ico b/exercises/02.server-components/04.problem.server-context/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/02.server-components/04.problem.server-context/public/favicon.ico differ diff --git a/exercises/02.server-components/04.problem.server-context/public/favicon.svg b/exercises/02.server-components/04.problem.server-context/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/02.server-components/04.problem.server-context/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/02.server-components/04.problem.server-context/public/iframe-sync.js b/exercises/02.server-components/04.problem.server-context/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/02.server-components/04.problem.server-context/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/broken-ship.webp b/exercises/02.server-components/04.problem.server-context/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/broken-ship.webp rename to exercises/02.server-components/04.problem.server-context/public/img/broken-ship.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/fallback-ship.png b/exercises/02.server-components/04.problem.server-context/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/fallback-ship.png rename to exercises/02.server-components/04.problem.server-context/public/img/fallback-ship.png diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/0268fc4817ad1.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/0268fc4817ad1.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/1ae7b4b92036b.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/1ae7b4b92036b.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/1ff1991efe029.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/1ff1991efe029.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/3ba8aa65ffe6c.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/441f7092a8d44.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/441f7092a8d44.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/5c13d8b28a14a.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/5c13d8b28a14a.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/627c497212456.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/627c497212456.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/670003aed3795.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/670003aed3795.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/6c86fca8b9086.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/6c86fca8b9086.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/6f375578ead88.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/6f375578ead88.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/ab267a5984523.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/ab267a5984523.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/b442531ea32b2.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/b442531ea32b2.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/bc4cbadf89bd3.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/bc4cbadf89bd3.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/cb03cc4e5717e.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/cb03cc4e5717e.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/cfd10fcd2de6c.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/cfd10fcd2de6c.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/d3b8aa65ffe6c.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/d486d48b82b81.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/d486d48b82b81.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/e92cefe4f6727.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/e92cefe4f6727.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/ec7a3f950f99f.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/ec7a3f950f99f.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/f3d9a88e1c234.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/f3d9a88e1c234.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/05.problem.bootstrap/public/img/ships/fdc13cb488bf1.webp b/exercises/02.server-components/04.problem.server-context/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/img/ships/fdc13cb488bf1.webp rename to exercises/02.server-components/04.problem.server-context/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/02.server-components/04.problem.server-context/public/index.html b/exercises/02.server-components/04.problem.server-context/public/index.html new file mode 100644 index 0000000..1649718 --- /dev/null +++ b/exercises/02.server-components/04.problem.server-context/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/05.problem.bootstrap/public/style.css b/exercises/02.server-components/04.problem.server-context/public/style.css similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/public/style.css rename to exercises/02.server-components/04.problem.server-context/public/style.css diff --git a/exercises/02.server-components/04.problem.server-context/server/app.js b/exercises/02.server-components/04.problem.server-context/server/app.js new file mode 100644 index 0000000..339949e --- /dev/null +++ b/exercises/02.server-components/04.problem.server-context/server/app.js @@ -0,0 +1,81 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +// ๐Ÿ’ฐ you'll want this: +// import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + // ๐Ÿจ rename this to data (again ๐Ÿ˜…) + const props = { shipId, search } + + // ๐Ÿจ wrap this ๐Ÿ‘‡ in shipDataStorage.run providing the data and remove the props from App + const { pipe } = renderToPipeableStream(h(App, props)) + pipe(context.env.outgoing) + // ๐Ÿจ wrap this ๐Ÿ‘† + + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/02.server-components/04.problem.server-context/tests/playwright.config.js b/exercises/02.server-components/04.problem.server-context/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/02.server-components/04.problem.server-context/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/02.server-components/04.problem.server-context/tests/solution.test.js b/exercises/02.server-components/04.problem.server-context/tests/solution.test.js new file mode 100644 index 0000000..c1ac38f --- /dev/null +++ b/exercises/02.server-components/04.problem.server-context/tests/solution.test.js @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + await filterInput.press('Enter') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + await page.waitForLoadState('networkidle') + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/02.server-components/04.problem.server-context/ui/app.js b/exercises/02.server-components/04.problem.server-context/ui/app.js new file mode 100644 index 0000000..221b857 --- /dev/null +++ b/exercises/02.server-components/04.problem.server-context/ui/app.js @@ -0,0 +1,69 @@ +import { Fragment, Suspense, createElement as h } from 'react' +// ๐Ÿ’ฐ you'll want this: +// import { shipDataStorage } from '../server/async-storage.js' +import { ShipDetails, ShipFallback } from './ship-details.js' +import { SearchResults, SearchResultsFallback } from './ship-search-results.js' + +export function App( + // ๐Ÿ’ฃ remove these props + { shipId, search }, +) { + // ๐Ÿจ use shipDataStorage.getStore() to access the shipId and search + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h( + Fragment, + null, + h( + 'form', + {}, + h('input', { + name: 'search', + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + autoFocus: true, + }), + ), + h( + 'ul', + null, + h( + Suspense, + { fallback: h(SearchResultsFallback) }, + h( + SearchResults, + // ๐Ÿ’ฃ remove the props here + { shipId, search }, + ), + ), + ), + ), + ), + h( + 'div', + { className: 'details' }, + shipId + ? h( + Suspense, + { + fallback: h( + ShipFallback, + // ๐Ÿ’ฃ remove the shipId prop here + { shipId }, + ), + }, + h( + ShipDetails, + // ๐Ÿ’ฃ remove the shipId prop here + { shipId }, + ), + ) + : h('p', null, 'Select a ship from the list to see details'), + ), + ) +} diff --git a/exercises/01.exercises/08.problem.hydrate/src/img-utils.js b/exercises/02.server-components/04.problem.server-context/ui/img-utils.js similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/src/img-utils.js rename to exercises/02.server-components/04.problem.server-context/ui/img-utils.js diff --git a/exercises/02.server-components/04.problem.server-context/ui/index.js b/exercises/02.server-components/04.problem.server-context/ui/index.js new file mode 100644 index 0000000..577ae9e --- /dev/null +++ b/exercises/02.server-components/04.problem.server-context/ui/index.js @@ -0,0 +1,35 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +import { createFromFetch } from 'react-server-dom-esm/client' +import { shipFallbackSrc } from './img-utils.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +const initialLocation = getGlobalLocation() +const initialContentFetchPromise = fetch(`/rsc${initialLocation}`) +const initialContentPromise = createFromFetch(initialContentFetchPromise) + +function Root() { + const content = use(initialContentPromise) + return content +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h(Root), + ), + ), + ) +}) diff --git a/exercises/02.server-components/04.problem.server-context/ui/ship-details.js b/exercises/02.server-components/04.problem.server-context/ui/ship-details.js new file mode 100644 index 0000000..ecb24e1 --- /dev/null +++ b/exercises/02.server-components/04.problem.server-context/ui/ship-details.js @@ -0,0 +1,91 @@ +import { createElement as h } from 'react' +// ๐Ÿ’ฐ you'll need this: +// import { shipDataStorage } from '../server/async-storage.js' +import { getShip } from '../db/ship-api.js' +import { getImageUrlForShip } from './img-utils.js' + +export async function ShipDetails( + // ๐Ÿ’ฃ remove the shipId prop + { shipId }, +) { + // ๐Ÿจ get the shipId from 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('img', { 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( + // ๐Ÿ’ฃ remove the shipId prop + { shipId }, +) { + // ๐Ÿจ get the shipId from shipDataStorage.getStore() + return h( + 'div', + { className: 'ship-info' }, + h( + 'div', + { className: 'ship-info__img-wrapper' }, + h('img', { + 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)')), + ), + ), + ), + ), + ) +} diff --git a/exercises/02.server-components/04.problem.server-context/ui/ship-search-results.js b/exercises/02.server-components/04.problem.server-context/ui/ship-search-results.js new file mode 100644 index 0000000..610681b --- /dev/null +++ b/exercises/02.server-components/04.problem.server-context/ui/ship-search-results.js @@ -0,0 +1,54 @@ +import { createElement as h } from 'react' +import { searchShips } from '../db/ship-api.js' +// ๐Ÿ’ฐ you'll want this +// import { shipDataStorage } from '../server/async-storage.js' +import { getImageUrlForShip, shipFallbackSrc } from './img-utils.js' + +export async function SearchResults( + // ๐Ÿ’ฃ remove the props here + { shipId: currentShipId, search }, +) { + // ๐Ÿจ get the shipId and search from shipDataStorage.getStore() + const shipResults = await searchShips({ search }) + return shipResults.ships.map((ship) => { + const href = [ + `/${ship.id}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h( + 'li', + { key: ship.name }, + h( + 'a', + { + href, + style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, + }, + h('img', { + 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/01.exercises/05.solution.bootstrap/.gitignore b/exercises/02.server-components/04.solution.server-context/.gitignore similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/.gitignore rename to exercises/02.server-components/04.solution.server-context/.gitignore diff --git a/exercises/02.server-components/04.solution.server-context/README.mdx b/exercises/02.server-components/04.solution.server-context/README.mdx new file mode 100644 index 0000000..29812dc --- /dev/null +++ b/exercises/02.server-components/04.solution.server-context/README.mdx @@ -0,0 +1,9 @@ +# Server Context + + + +๐Ÿ‘จโ€๐Ÿ’ผ Great work! Now we have access to request data (like the `shipId` param and +the `search` query param) through our `AsyncLocalStorage` object. + +๐Ÿฆ‰ You could definitely take this too far, but it works quite well for global +things like this. diff --git a/exercises/02.server-components/04.solution.server-context/db/ship-api.js b/exercises/02.server-components/04.solution.server-context/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/02.server-components/04.solution.server-context/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/02.server-components/04.solution.server-context/db/ships.json b/exercises/02.server-components/04.solution.server-context/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/02.server-components/04.solution.server-context/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/02.server-components/04.solution.server-context/package.json b/exercises/02.server-components/04.solution.server-context/package.json new file mode 100644 index 0000000..4be4853 --- /dev/null +++ b/exercises/02.server-components/04.solution.server-context/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__02.server-components__sep__04.solution.server-context", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/02.server-components/04.solution.server-context/public/favicon.ico b/exercises/02.server-components/04.solution.server-context/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/02.server-components/04.solution.server-context/public/favicon.ico differ diff --git a/exercises/02.server-components/04.solution.server-context/public/favicon.svg b/exercises/02.server-components/04.solution.server-context/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/02.server-components/04.solution.server-context/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/02.server-components/04.solution.server-context/public/iframe-sync.js b/exercises/02.server-components/04.solution.server-context/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/02.server-components/04.solution.server-context/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/broken-ship.webp b/exercises/02.server-components/04.solution.server-context/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/broken-ship.webp rename to exercises/02.server-components/04.solution.server-context/public/img/broken-ship.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/fallback-ship.png b/exercises/02.server-components/04.solution.server-context/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/fallback-ship.png rename to exercises/02.server-components/04.solution.server-context/public/img/fallback-ship.png diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/0268fc4817ad1.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/0268fc4817ad1.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/1ae7b4b92036b.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/1ae7b4b92036b.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/1ff1991efe029.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/1ff1991efe029.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/3ba8aa65ffe6c.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/441f7092a8d44.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/441f7092a8d44.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/5c13d8b28a14a.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/5c13d8b28a14a.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/627c497212456.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/627c497212456.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/670003aed3795.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/670003aed3795.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/6c86fca8b9086.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/6c86fca8b9086.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/6f375578ead88.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/6f375578ead88.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/ab267a5984523.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/ab267a5984523.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/b442531ea32b2.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/b442531ea32b2.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/bc4cbadf89bd3.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/bc4cbadf89bd3.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/cb03cc4e5717e.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/cb03cc4e5717e.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/cfd10fcd2de6c.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/cfd10fcd2de6c.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/d3b8aa65ffe6c.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/d486d48b82b81.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/d486d48b82b81.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/e92cefe4f6727.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/e92cefe4f6727.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/ec7a3f950f99f.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/ec7a3f950f99f.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/f3d9a88e1c234.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/f3d9a88e1c234.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/05.solution.bootstrap/public/img/ships/fdc13cb488bf1.webp b/exercises/02.server-components/04.solution.server-context/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/img/ships/fdc13cb488bf1.webp rename to exercises/02.server-components/04.solution.server-context/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/02.server-components/04.solution.server-context/public/index.html b/exercises/02.server-components/04.solution.server-context/public/index.html new file mode 100644 index 0000000..1649718 --- /dev/null +++ b/exercises/02.server-components/04.solution.server-context/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/05.solution.bootstrap/public/style.css b/exercises/02.server-components/04.solution.server-context/public/style.css similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/public/style.css rename to exercises/02.server-components/04.solution.server-context/public/style.css diff --git a/exercises/02.server-components/04.solution.server-context/server/app.js b/exercises/02.server-components/04.solution.server-context/server/app.js new file mode 100644 index 0000000..450c6bb --- /dev/null +++ b/exercises/02.server-components/04.solution.server-context/server/app.js @@ -0,0 +1,77 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const { pipe } = renderToPipeableStream(h(App)) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/02.solution.server-context/server/async-storage.js b/exercises/02.server-components/04.solution.server-context/server/async-storage.js similarity index 100% rename from exercises/01.exercises/02.solution.server-context/server/async-storage.js rename to exercises/02.server-components/04.solution.server-context/server/async-storage.js diff --git a/exercises/02.server-components/04.solution.server-context/tests/playwright.config.js b/exercises/02.server-components/04.solution.server-context/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/02.server-components/04.solution.server-context/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/02.server-components/04.solution.server-context/tests/solution.test.js b/exercises/02.server-components/04.solution.server-context/tests/solution.test.js new file mode 100644 index 0000000..c1ac38f --- /dev/null +++ b/exercises/02.server-components/04.solution.server-context/tests/solution.test.js @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + await filterInput.press('Enter') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + await page.waitForLoadState('networkidle') + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/02.server-components/04.solution.server-context/ui/app.js b/exercises/02.server-components/04.solution.server-context/ui/app.js new file mode 100644 index 0000000..7df8dc7 --- /dev/null +++ b/exercises/02.server-components/04.solution.server-context/ui/app.js @@ -0,0 +1,43 @@ +import { Fragment, Suspense, createElement as h } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ShipDetails, ShipFallback } from './ship-details.js' +import { SearchResults, SearchResultsFallback } from './ship-search-results.js' + +export function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h( + Fragment, + null, + h( + 'form', + {}, + h('input', { + name: 'search', + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + autoFocus: true, + }), + ), + h( + 'ul', + null, + h(Suspense, { fallback: h(SearchResultsFallback) }, h(SearchResults)), + ), + ), + ), + h( + 'div', + { className: 'details' }, + shipId + ? h(Suspense, { fallback: h(ShipFallback) }, h(ShipDetails)) + : h('p', null, 'Select a ship from the list to see details'), + ), + ) +} diff --git a/exercises/01.exercises/08.solution.hydrate/src/img-utils.js b/exercises/02.server-components/04.solution.server-context/ui/img-utils.js similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/src/img-utils.js rename to exercises/02.server-components/04.solution.server-context/ui/img-utils.js diff --git a/exercises/02.server-components/04.solution.server-context/ui/index.js b/exercises/02.server-components/04.solution.server-context/ui/index.js new file mode 100644 index 0000000..577ae9e --- /dev/null +++ b/exercises/02.server-components/04.solution.server-context/ui/index.js @@ -0,0 +1,35 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +import { createFromFetch } from 'react-server-dom-esm/client' +import { shipFallbackSrc } from './img-utils.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +const initialLocation = getGlobalLocation() +const initialContentFetchPromise = fetch(`/rsc${initialLocation}`) +const initialContentPromise = createFromFetch(initialContentFetchPromise) + +function Root() { + const content = use(initialContentPromise) + return content +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h(Root), + ), + ), + ) +}) diff --git a/exercises/02.server-components/04.solution.server-context/ui/ship-details.js b/exercises/02.server-components/04.solution.server-context/ui/ship-details.js new file mode 100644 index 0000000..14ba880 --- /dev/null +++ b/exercises/02.server-components/04.solution.server-context/ui/ship-details.js @@ -0,0 +1,84 @@ +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' + +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('img', { 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('img', { + 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)')), + ), + ), + ), + ), + ) +} diff --git a/exercises/02.server-components/04.solution.server-context/ui/ship-search-results.js b/exercises/02.server-components/04.solution.server-context/ui/ship-search-results.js new file mode 100644 index 0000000..69be0af --- /dev/null +++ b/exercises/02.server-components/04.solution.server-context/ui/ship-search-results.js @@ -0,0 +1,50 @@ +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' + +export async function SearchResults() { + const { shipId: currentShipId, search } = shipDataStorage.getStore() + const shipResults = await searchShips({ search }) + return shipResults.ships.map((ship) => { + const href = [ + `/${ship.id}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h( + 'li', + { key: ship.name }, + h( + 'a', + { + href, + style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, + }, + h('img', { + 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/02.server-components/FINISHED.mdx b/exercises/02.server-components/FINISHED.mdx new file mode 100644 index 0000000..3e797c5 --- /dev/null +++ b/exercises/02.server-components/FINISHED.mdx @@ -0,0 +1,8 @@ +# Server Components + + + +๐Ÿ‘จโ€๐Ÿ’ผ It's cool how the RSC work the React team fits in so nicely with the async +primitives React has to offer like `use` and `Suspense`. It also works nicely +with error boundaries, but those require client-side code so we'll get to that +next! diff --git a/exercises/02.server-components/README.mdx b/exercises/02.server-components/README.mdx new file mode 100644 index 0000000..d3200a1 --- /dev/null +++ b/exercises/02.server-components/README.mdx @@ -0,0 +1,175 @@ +# Server Components + + + +The vast majority of React applications have both of these issues: + +1. Data is subject to either cascading waterfalls or prop drilling. +2. JavaScript is sent to the client to hydrate components that are not + interactive. + +Depending on your situation, these two issues can be minimal and not worth +solving or they can be a major pain point. For most of you, it's probably the +latter. + +React server components solves both of these problems. By pushing the UI we +generate to the server, we reduce the JavaScript sent to the client down to only +the interactive bits and we also enable data fetching from the components +directly. + +The idea behind RSCs is conceptually simple. Instead of requesting JSON data and +handing that off to our components to generate the UI, we request the UI itself. + +Let's compare initial render of a SPA that uses JSON with a SPA that uses RSCs: + +![A flowchart for the initial render of a Typical SPA as described below](/images/spa-initial-render.png) + +
+Here's a bullet-point text version of this flowchart: + +- User goes to site + + - Browser requests document + - Server responds with document + - Browser renders loading spinner + - Browser requests client code + - Server responds with client code + - Browser updates UI components + - Browser requests data + - Server generates data response + - Browser sends JSON + - Browser updates UI with JSON data + +
+ +![A flowchart for React Server Components and Functions as described below](/images/super-simple-rsc-initial-render.png) + +
+Here's a bullet-point text version of the flowchart: + +- User goes to site + + - Browser requests document + - Server responds with document + - Browser renders Suspense fallback + - Browser requests JSX payload + - Server generates Serialized JSX with `react-server-dom-esm/server.renderToPipeableStream` + - Server streams Serialized JSX + - Browser renders streamed UI with `react-server-dom-esm/client.createFromFetch` + - Browser requests client component code + - Server responds with client component code + - Browser hydrates client components + +
+ +You'll notice these two flows are pretty similar. The biggest difference is in +where we generate the UI. + +In the SPA case, UI = data + components on the client. + +In the RSC case, UI = data + components on the server. + +Of course, there are some parts of our UI that need to be interactive and +that requires some of our code to be sent to the client. We'll cover this part +in a future exercise. This is important because it's the reason React needs its +own serialization format and can't just use HTML. + +## RSC Format + +Typically when you server render React, you render a string of HTML. However, +React Server Components need to have a good mechanism for mixing components for +client-side interactivity with components that are rendered on the server only. +Additionally, React Server Actions also necessitate a way to send a reference +to the client-side component that will be hydrated. + +To top it off, this all needs to be done in a way that allows for streaming the +UI to the client in any order. + +So React Server Components use a custom serialization format that looks like +this: + +```json nolang nonumber +1:D{"name":"App","env":"Server"} +0:{"returnValue":null,"root":"$L1"} +2:I["/ship-search.js","ShipSearch"] +5:I["/ship-details-pending.js","ShipDetailsPendingTransition"] +6:I["/error-boundary.js","ErrorBoundary"] +8:"$Sreact.suspense" +a:I["/img.js","ShipImg"] +3:D{"name":"SearchResults","env":"Server"} +4:D{"name":"SearchResultsFallback","env":"Server"} +4:[["$","li","0",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","1",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","2",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","3",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","4",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","5",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","6",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","7",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","8",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","9",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","10",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","11",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}]] +7:D{"name":"ShipError","env":"Server"} +7:["$","div",null,{"className":"ship-info","children":[["$","div",null,{"className":"ship-info__img-wrapper","children":["$","img",null,{"src":"/img/broken-ship.webp","alt":"broken ship"}]}],["$","section",null,{"children":["$","h2",null,{"children":"There was an error"}]}],["$","section",null,{"children":["There was an error loading \"","0268fc4817ad1","\""]}]]}] +9:D{"name":"ShipFallback","env":"Server"} +9:["$","div",null,{"className":"ship-info","children":[["$","div",null,{"className":"ship-info__img-wrapper","children":["$","$La",null,{"src":"/img/ships/0268fc4817ad1.webp?size=200","alt":"0268fc4817ad1"}]}],["$","section",null,{"children":["$","h2",null,{"children":"Loading..."}]}],["$","div",null,{"children":["Top Speed: XX"," ",["$","small",null,{"children":"lyh"}]]}],["$","section",null,{"children":["$","ul",null,{"children":[["$","li","0",{"children":[["$","label",null,{"children":"loading"}],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"}]]}]]}],["$","li","1",{"children":[["$","label",null,{"children":"loading"}],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"}]]}]]}],["$","li","2",{"children":[["$","label",null,{"children":"loading"}],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"}]]}]]}]]}]}]]}] +b:D{"name":"ShipDetails","env":"Server"} +1:["$","div",null,{"className":"app","children":[["$","div",null,{"className":"search","children":["$","$L2",null,{"search":"m","results":"$L3","fallback":"$4"}]}],["$","$L5",null,{"children":["$","$L6",null,{"fallback":"$7","children":["$","$8",null,{"fallback":"$9","children":"$Lb"}]}]}]]}] +c:I["/ship-search.js","SelectShipLink"] +3:[["$","li","Bomber",{"children":["$","$Lc",null,{"shipId":"5c13d8b28a14a","highlight":false,"children":[["$","$La",null,{"src":"/img/ships/5c13d8b28a14a.webp?size=20","alt":"Bomber"}],"Bomber"]}]}],["$","li","Diplomatic Vessel",{"children":["$","$Lc",null,{"shipId":"6f375578ead88","highlight":false,"children":[["$","$La",null,{"src":"/img/ships/6f375578ead88.webp?size=20","alt":"Diplomatic Vessel"}],"Diplomatic Vessel"]}]}],["$","li","Mining Ship",{"children":["$","$Lc",null,{"shipId":"627c497212456","highlight":false,"children":[["$","$La",null,{"src":"/img/ships/627c497212456.webp?size=20","alt":"Mining Ship"}],"Mining Ship"]}]}],["$","li","Medical Ship",{"children":["$","$Lc",null,{"shipId":"0268fc4817ad1","highlight":true,"children":[["$","$La",null,{"src":"/img/ships/0268fc4817ad1.webp?size=20","alt":"Medical Ship"}],"Medical Ship"]}]}]] +b:["$","div",null,{"className":"ship-info","children":[["$","div",null,{"className":"ship-info__img-wrapper","children":["$","$La",null,{"src":"/img/ships/0268fc4817ad1.webp?size=200","alt":"Medical Ship"}]}],["$","section",null,{"children":["$","h2",null,{"children":"Medical Ship"}]}],["$","div",null,{"children":["Top Speed: ",2," ",["$","small",null,{"children":"lyh"}]]}],["$","section",null,{"children":["$","p",null,{"children":"NOTE: This ship is not equipped with any weapons."}]}]]}] +``` + +This is not something you would want to write by hand. Instead, you'll use the +`react-server-dom-esm/server`'s `renderToPipeableStream` function to generate +this for you. Then on the client-side, `react-server-dom-esm/client`'s +`createFromFetch` will take care of converting this format into React elements +that can be rendered by React in the browser. + + + The `react-server-dom-esm` package is one of many such packages for generating + and consuming React Server Components. + + + + To explore this format further, you can check out [this blog + post](https://www.alvar.dev/blog/creating-devtools-for-react-server-components) + that goes in depth on the format and the [accompanying + tool](https://rsc-parser.vercel.app/) that can help you visualize different + aspects of the format. + + +## Putting it together + +So on the server side, you have some code that generates the serialized JSX in +the RSC format and on the client side, you have some code that converts the +serialized JSX into React elements: + +```js filename=server.js +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' + +// some server code... + +app.get('/rsc', (context) => { + const { pipe } = renderToPipeableStream(h(App)) + pipe(context.env.outgoing) + return RESPONSE_ALREADY_SENT +}) +``` + +```js filename=client.js +import { createFromFetch } from 'react-server-dom-esm/client' + +// some client code... + +const responsePromise = fetch('/rsc') +const ui = await createFromFetch(responsePromise) +createRoot(document.getElementById('root')).render(ui) +``` + +Now you combine this with some nice suspense code on the client for handling the +loading state and you've got yourself a nice RSC setup. + +In this world, it doesn't matter how many components you have or how heavy those +dependencies are. They'll never get sent over the wire. Just the UI that they +generated. + + + In some situations, it's possible sending the data and components will be less + than sending the generated UI. But the size of the payload is only one aspect + to consider. Parsing and executing JavaScript on the client requires more + resources than handling the stream of RSCs. Additionally, RSCs enable + composition of data requirements and UI generation in a way that is not + possible with JSON data. + diff --git a/exercises/01.exercises/06.problem.import-map/.gitignore b/exercises/03.client-components/01.problem.loader/.gitignore similarity index 100% rename from exercises/01.exercises/06.problem.import-map/.gitignore rename to exercises/03.client-components/01.problem.loader/.gitignore diff --git a/exercises/03.client-components/01.problem.loader/README.mdx b/exercises/03.client-components/01.problem.loader/README.mdx new file mode 100644 index 0000000..da55b77 --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/README.mdx @@ -0,0 +1,33 @@ +# Node.js Loader + + + +๐Ÿ‘จโ€๐Ÿ’ผ Alright, we're going to get our server ready to start handling `'use client'` +modules because we want to add some client-side interactivity to our app. + +We're planning on adding the ability to change the ship names by clicking on +them. The idea is, the ship name is a button and when you click on it, it'll +change to a form with an input which you can then submit and that will update +the name. We'll get to the actual implementation for updating later, but for now +we just need to get our client-side code for the edit state working. + +๐Ÿงโ€โ™‚๏ธ I've created which you'll need to +register with Node.js as a loader for turning `'use client'` module exports into +reference registrations. + +๐Ÿ‘จโ€๐Ÿ’ผ Thanks Kellie. So now, what you need to do is register the loader, update the +`dev` script in to import the loader +registration. + +```sh nonumber nolang +node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js +``` + +Now update the to add `'use client'` to +the top of the module. + +When you're finished, we'll not actually be loading modules yet, but we'll be +almost ready to do it! I recommend you add a couple console logs so you can +observe what the loader does to our `'use client'` module exports. + +๐Ÿ’ฐ I've put a couple good ones in place for you. diff --git a/exercises/03.client-components/01.problem.loader/db/ship-api.js b/exercises/03.client-components/01.problem.loader/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/03.client-components/01.problem.loader/db/ships.json b/exercises/03.client-components/01.problem.loader/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/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/03.client-components/01.problem.loader/package.json b/exercises/03.client-components/01.problem.loader/package.json new file mode 100644 index 0000000..14d8c62 --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__03.client-components__sep__01.problem.loader", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/03.client-components/01.problem.loader/public/favicon.ico b/exercises/03.client-components/01.problem.loader/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/03.client-components/01.problem.loader/public/favicon.ico differ diff --git a/exercises/03.client-components/01.problem.loader/public/favicon.svg b/exercises/03.client-components/01.problem.loader/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/03.client-components/01.problem.loader/public/iframe-sync.js b/exercises/03.client-components/01.problem.loader/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/06.problem.import-map/public/img/broken-ship.webp b/exercises/03.client-components/01.problem.loader/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/broken-ship.webp rename to exercises/03.client-components/01.problem.loader/public/img/broken-ship.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/fallback-ship.png b/exercises/03.client-components/01.problem.loader/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/fallback-ship.png rename to exercises/03.client-components/01.problem.loader/public/img/fallback-ship.png diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/0268fc4817ad1.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/0268fc4817ad1.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/1ae7b4b92036b.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/1ae7b4b92036b.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/1ff1991efe029.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/1ff1991efe029.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/3ba8aa65ffe6c.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/441f7092a8d44.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/441f7092a8d44.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/5c13d8b28a14a.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/5c13d8b28a14a.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/627c497212456.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/627c497212456.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/670003aed3795.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/670003aed3795.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/6c86fca8b9086.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/6c86fca8b9086.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/6f375578ead88.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/6f375578ead88.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/ab267a5984523.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/ab267a5984523.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/b442531ea32b2.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/b442531ea32b2.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/bc4cbadf89bd3.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/bc4cbadf89bd3.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/cb03cc4e5717e.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/cb03cc4e5717e.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/cfd10fcd2de6c.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/cfd10fcd2de6c.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/d3b8aa65ffe6c.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/d486d48b82b81.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/d486d48b82b81.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/e92cefe4f6727.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/e92cefe4f6727.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/ec7a3f950f99f.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/ec7a3f950f99f.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/f3d9a88e1c234.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/f3d9a88e1c234.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/06.problem.import-map/public/img/ships/fdc13cb488bf1.webp b/exercises/03.client-components/01.problem.loader/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/img/ships/fdc13cb488bf1.webp rename to exercises/03.client-components/01.problem.loader/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/03.client-components/01.problem.loader/public/index.html b/exercises/03.client-components/01.problem.loader/public/index.html new file mode 100644 index 0000000..1649718 --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/06.problem.import-map/public/style.css b/exercises/03.client-components/01.problem.loader/public/style.css similarity index 100% rename from exercises/01.exercises/06.problem.import-map/public/style.css rename to exercises/03.client-components/01.problem.loader/public/style.css diff --git a/exercises/03.client-components/01.problem.loader/server/app.js b/exercises/03.client-components/01.problem.loader/server/app.js new file mode 100644 index 0000000..450c6bb --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/server/app.js @@ -0,0 +1,77 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const { pipe } = renderToPipeableStream(h(App)) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/03.problem.url/server/async-storage.js b/exercises/03.client-components/01.problem.loader/server/async-storage.js similarity index 100% rename from exercises/01.exercises/03.problem.url/server/async-storage.js rename to exercises/03.client-components/01.problem.loader/server/async-storage.js diff --git a/exercises/03.client-components/01.problem.loader/server/register-rsc-loader.js b/exercises/03.client-components/01.problem.loader/server/register-rsc-loader.js new file mode 100644 index 0000000..0446ad9 --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/server/register-rsc-loader.js @@ -0,0 +1,5 @@ +// ๐Ÿจ register our custom rsc-loader.js module as a custom loader +// ๐Ÿ’ฐ this isn't a node.js workshop, so here you go +// import { register } from 'node:module' + +// register('./rsc-loader.js', import.meta.url) diff --git a/exercises/03.client-components/01.problem.loader/server/rsc-loader.js b/exercises/03.client-components/01.problem.loader/server/rsc-loader.js new file mode 100644 index 0000000..acec3ef --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/server/rsc-loader.js @@ -0,0 +1,30 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + // ๐Ÿจ uncomment this so you can observe the changes the loader makes to our + // edit-text.js module. + // ๐Ÿ’ฐ + // if (url.includes('edit-text')) { + // console.log(result.source) + // } + return result +} diff --git a/exercises/03.client-components/01.problem.loader/tests/playwright.config.js b/exercises/03.client-components/01.problem.loader/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/03.client-components/01.problem.loader/tests/smoke.test.js b/exercises/03.client-components/01.problem.loader/tests/smoke.test.js new file mode 100644 index 0000000..c1ac38f --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/tests/smoke.test.js @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + await filterInput.press('Enter') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + await page.waitForLoadState('networkidle') + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/03.client-components/01.problem.loader/ui/app.js b/exercises/03.client-components/01.problem.loader/ui/app.js new file mode 100644 index 0000000..7df8dc7 --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/ui/app.js @@ -0,0 +1,43 @@ +import { Fragment, Suspense, createElement as h } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ShipDetails, ShipFallback } from './ship-details.js' +import { SearchResults, SearchResultsFallback } from './ship-search-results.js' + +export function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h( + Fragment, + null, + h( + 'form', + {}, + h('input', { + name: 'search', + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + autoFocus: true, + }), + ), + h( + 'ul', + null, + h(Suspense, { fallback: h(SearchResultsFallback) }, h(SearchResults)), + ), + ), + ), + h( + 'div', + { className: 'details' }, + shipId + ? h(Suspense, { fallback: h(ShipFallback) }, h(ShipDetails)) + : h('p', null, 'Select a ship from the list to see details'), + ), + ) +} diff --git a/exercises/03.client-components/01.problem.loader/ui/edit-text.js b/exercises/03.client-components/01.problem.loader/ui/edit-text.js new file mode 100644 index 0000000..af0cb7d --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/ui/edit-text.js @@ -0,0 +1,84 @@ +// ๐Ÿ’ฐ add the 'use client' directive here + +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + null, + edit + ? h( + 'form', + { + onSubmit: (event) => { + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + ) +} diff --git a/exercises/01.exercises/09.problem.routing/src/img-utils.js b/exercises/03.client-components/01.problem.loader/ui/img-utils.js similarity index 100% rename from exercises/01.exercises/09.problem.routing/src/img-utils.js rename to exercises/03.client-components/01.problem.loader/ui/img-utils.js diff --git a/exercises/03.client-components/01.problem.loader/ui/index.js b/exercises/03.client-components/01.problem.loader/ui/index.js new file mode 100644 index 0000000..577ae9e --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/ui/index.js @@ -0,0 +1,35 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +import { createFromFetch } from 'react-server-dom-esm/client' +import { shipFallbackSrc } from './img-utils.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +const initialLocation = getGlobalLocation() +const initialContentFetchPromise = fetch(`/rsc${initialLocation}`) +const initialContentPromise = createFromFetch(initialContentFetchPromise) + +function Root() { + const content = use(initialContentPromise) + return content +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h(Root), + ), + ), + ) +}) diff --git a/exercises/03.client-components/01.problem.loader/ui/ship-details.js b/exercises/03.client-components/01.problem.loader/ui/ship-details.js new file mode 100644 index 0000000..193fe75 --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/ui/ship-details.js @@ -0,0 +1,104 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +// ๐Ÿจ import the EditableText component from the client module +// import { EditableText } from './edit-text.js' +import { getImageUrlForShip } from './img-utils.js' + +// ๐Ÿจ log the EditableText to the console so you can see what the server sees +// ๐Ÿ’ฐ This will log the value itself as well as all the properties +// const properties = {} +// for (const [key, descriptor] of Object.entries( +// Object.getOwnPropertyDescriptors(EditableText), +// )) { +// properties[key] = descriptor.value +// } + +// console.log(EditableText.toString()) +// console.log( +// JSON.stringify( +// properties, +// (key, value) => (typeof value === 'object' ? value : String(value)), +// 2, +// ), +// ) + +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('img', { 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('img', { + 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)')), + ), + ), + ), + ), + ) +} diff --git a/exercises/03.client-components/01.problem.loader/ui/ship-search-results.js b/exercises/03.client-components/01.problem.loader/ui/ship-search-results.js new file mode 100644 index 0000000..69be0af --- /dev/null +++ b/exercises/03.client-components/01.problem.loader/ui/ship-search-results.js @@ -0,0 +1,50 @@ +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' + +export async function SearchResults() { + const { shipId: currentShipId, search } = shipDataStorage.getStore() + const shipResults = await searchShips({ search }) + return shipResults.ships.map((ship) => { + const href = [ + `/${ship.id}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h( + 'li', + { key: ship.name }, + h( + 'a', + { + href, + style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, + }, + h('img', { + 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/01.exercises/06.solution.import-map/.gitignore b/exercises/03.client-components/01.solution.loader/.gitignore similarity index 100% rename from exercises/01.exercises/06.solution.import-map/.gitignore rename to exercises/03.client-components/01.solution.loader/.gitignore diff --git a/exercises/03.client-components/01.solution.loader/README.mdx b/exercises/03.client-components/01.solution.loader/README.mdx new file mode 100644 index 0000000..f8db81c --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/README.mdx @@ -0,0 +1,61 @@ +# Node.js Loader + + + +๐Ÿ‘จโ€๐Ÿ’ผ Great job! + +๐Ÿฆ‰ So when Node.js imports the file, +instead of this: + +```tsx +'use client' +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +// ... + +export function EditableText({ id, shipId, initialValue = '' }) { + // ... +} +``` + +It's getting this: + +{/* prettier-ignore */} +```js nonumber nolang +import {registerClientReference} from "react-server-dom-esm/server"; +export const EditableText = registerClientReference(function() {throw new Error("Attempted to call EditableText() from the server but EditableText is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.");},"file:///Users/kentcdodds/code/epicweb-dev/react-server-components/playground/ui/edit-text.js","EditableText"); +``` + +And when I `console.log(EditableText.toString())` +in , I'm getting this: + +{/* prettier-ignore */} +```js nonumber nolang +function() {throw new Error("Attempted to call EditableText() from the server but EditableText is on the client. It's not possible to invoke a client function from the server, it can only be rendered as a Component or passed to props of a Client Component.");} +``` + +Meaning if I try to call it in the RSC environment, I get an error. + +(If you added the logs, you should have seen something like that in the +console). That transformation is happening thanks to the loader you've +registered. + +The important elements here are: + +1. All exports of this module are wrapped in `registerClientReference`. +2. The error message is thrown when the function is called on the server + (because it should not be). +3. The path to the file is passed as the second argument to `registerClientReference`. +4. The name of the export is passed as the third argument to `registerClientReference`. + +The `registerClientReference` function uses the path and name to generate a +unique identifier for the client-side function. This identifier is used to +generate a reference when rendering our server components that use these client +components. + +No build tool. Just built-in runtime features of Node.js. Cool huh!? + +๐Ÿงโ€โ™‚๏ธ I'm going to add some error boundaries in our app to get it ready for the +work you're going to do. Check out my changes if +you like. diff --git a/exercises/03.client-components/01.solution.loader/db/ship-api.js b/exercises/03.client-components/01.solution.loader/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/03.client-components/01.solution.loader/db/ships.json b/exercises/03.client-components/01.solution.loader/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/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/03.client-components/01.solution.loader/package.json b/exercises/03.client-components/01.solution.loader/package.json new file mode 100644 index 0000000..2504a2f --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__03.client-components__sep__01.solution.loader", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/03.client-components/01.solution.loader/public/favicon.ico b/exercises/03.client-components/01.solution.loader/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/03.client-components/01.solution.loader/public/favicon.ico differ diff --git a/exercises/03.client-components/01.solution.loader/public/favicon.svg b/exercises/03.client-components/01.solution.loader/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/03.client-components/01.solution.loader/public/iframe-sync.js b/exercises/03.client-components/01.solution.loader/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/06.solution.import-map/public/img/broken-ship.webp b/exercises/03.client-components/01.solution.loader/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/broken-ship.webp rename to exercises/03.client-components/01.solution.loader/public/img/broken-ship.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/fallback-ship.png b/exercises/03.client-components/01.solution.loader/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/fallback-ship.png rename to exercises/03.client-components/01.solution.loader/public/img/fallback-ship.png diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/0268fc4817ad1.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/0268fc4817ad1.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/1ae7b4b92036b.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/1ae7b4b92036b.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/1ff1991efe029.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/1ff1991efe029.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/3ba8aa65ffe6c.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/441f7092a8d44.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/441f7092a8d44.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/5c13d8b28a14a.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/5c13d8b28a14a.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/627c497212456.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/627c497212456.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/670003aed3795.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/670003aed3795.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/6c86fca8b9086.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/6c86fca8b9086.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/6f375578ead88.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/6f375578ead88.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/ab267a5984523.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/ab267a5984523.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/b442531ea32b2.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/b442531ea32b2.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/bc4cbadf89bd3.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/bc4cbadf89bd3.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/cb03cc4e5717e.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/cb03cc4e5717e.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/cfd10fcd2de6c.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/cfd10fcd2de6c.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/d3b8aa65ffe6c.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/d486d48b82b81.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/d486d48b82b81.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/e92cefe4f6727.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/e92cefe4f6727.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/ec7a3f950f99f.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/ec7a3f950f99f.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/f3d9a88e1c234.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/f3d9a88e1c234.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/06.solution.import-map/public/img/ships/fdc13cb488bf1.webp b/exercises/03.client-components/01.solution.loader/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/img/ships/fdc13cb488bf1.webp rename to exercises/03.client-components/01.solution.loader/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/03.client-components/01.solution.loader/public/index.html b/exercises/03.client-components/01.solution.loader/public/index.html new file mode 100644 index 0000000..1649718 --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/06.solution.import-map/public/style.css b/exercises/03.client-components/01.solution.loader/public/style.css similarity index 100% rename from exercises/01.exercises/06.solution.import-map/public/style.css rename to exercises/03.client-components/01.solution.loader/public/style.css diff --git a/exercises/03.client-components/01.solution.loader/server/app.js b/exercises/03.client-components/01.solution.loader/server/app.js new file mode 100644 index 0000000..450c6bb --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/server/app.js @@ -0,0 +1,77 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const { pipe } = renderToPipeableStream(h(App)) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/03.solution.url/server/async-storage.js b/exercises/03.client-components/01.solution.loader/server/async-storage.js similarity index 100% rename from exercises/01.exercises/03.solution.url/server/async-storage.js rename to exercises/03.client-components/01.solution.loader/server/async-storage.js diff --git a/exercises/01.exercises/08.solution.hydrate/server/register-rsc-loader.js b/exercises/03.client-components/01.solution.loader/server/register-rsc-loader.js similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/server/register-rsc-loader.js rename to exercises/03.client-components/01.solution.loader/server/register-rsc-loader.js diff --git a/exercises/03.client-components/01.solution.loader/server/rsc-loader.js b/exercises/03.client-components/01.solution.loader/server/rsc-loader.js new file mode 100644 index 0000000..acdf571 --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/server/rsc-loader.js @@ -0,0 +1,27 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + if (url.includes('edit-text')) { + console.log(result.source) + } + return result +} diff --git a/exercises/03.client-components/01.solution.loader/tests/playwright.config.js b/exercises/03.client-components/01.solution.loader/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/03.client-components/01.solution.loader/tests/smoke.test.js b/exercises/03.client-components/01.solution.loader/tests/smoke.test.js new file mode 100644 index 0000000..c1ac38f --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/tests/smoke.test.js @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + await filterInput.press('Enter') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + await page.waitForLoadState('networkidle') + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/03.client-components/01.solution.loader/ui/app.js b/exercises/03.client-components/01.solution.loader/ui/app.js new file mode 100644 index 0000000..7df8dc7 --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/ui/app.js @@ -0,0 +1,43 @@ +import { Fragment, Suspense, createElement as h } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ShipDetails, ShipFallback } from './ship-details.js' +import { SearchResults, SearchResultsFallback } from './ship-search-results.js' + +export function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h( + Fragment, + null, + h( + 'form', + {}, + h('input', { + name: 'search', + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + autoFocus: true, + }), + ), + h( + 'ul', + null, + h(Suspense, { fallback: h(SearchResultsFallback) }, h(SearchResults)), + ), + ), + ), + h( + 'div', + { className: 'details' }, + shipId + ? h(Suspense, { fallback: h(ShipFallback) }, h(ShipDetails)) + : h('p', null, 'Select a ship from the list to see details'), + ), + ) +} diff --git a/exercises/03.client-components/01.solution.loader/ui/edit-text.js b/exercises/03.client-components/01.solution.loader/ui/edit-text.js new file mode 100644 index 0000000..b30c19f --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/ui/edit-text.js @@ -0,0 +1,84 @@ +'use client' + +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + null, + edit + ? h( + 'form', + { + onSubmit: (event) => { + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + ) +} diff --git a/exercises/01.exercises/09.solution.routing/src/img-utils.js b/exercises/03.client-components/01.solution.loader/ui/img-utils.js similarity index 100% rename from exercises/01.exercises/09.solution.routing/src/img-utils.js rename to exercises/03.client-components/01.solution.loader/ui/img-utils.js diff --git a/exercises/03.client-components/01.solution.loader/ui/index.js b/exercises/03.client-components/01.solution.loader/ui/index.js new file mode 100644 index 0000000..577ae9e --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/ui/index.js @@ -0,0 +1,35 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +import { createFromFetch } from 'react-server-dom-esm/client' +import { shipFallbackSrc } from './img-utils.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +const initialLocation = getGlobalLocation() +const initialContentFetchPromise = fetch(`/rsc${initialLocation}`) +const initialContentPromise = createFromFetch(initialContentFetchPromise) + +function Root() { + const content = use(initialContentPromise) + return content +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h(Root), + ), + ), + ) +}) diff --git a/exercises/03.client-components/01.solution.loader/ui/ship-details.js b/exercises/03.client-components/01.solution.loader/ui/ship-details.js new file mode 100644 index 0000000..63e7d9b --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/ui/ship-details.js @@ -0,0 +1,101 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { EditableText } from './edit-text.js' +import { getImageUrlForShip } from './img-utils.js' + +const properties = {} +for (const [key, descriptor] of Object.entries( + Object.getOwnPropertyDescriptors(EditableText), +)) { + properties[key] = descriptor.value +} + +console.log(EditableText.toString()) +console.log( + JSON.stringify( + properties, + (key, value) => (typeof value === 'object' ? value : String(value)), + 2, + ), +) + +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('img', { 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('img', { + 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)')), + ), + ), + ), + ), + ) +} diff --git a/exercises/03.client-components/01.solution.loader/ui/ship-search-results.js b/exercises/03.client-components/01.solution.loader/ui/ship-search-results.js new file mode 100644 index 0000000..69be0af --- /dev/null +++ b/exercises/03.client-components/01.solution.loader/ui/ship-search-results.js @@ -0,0 +1,50 @@ +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' + +export async function SearchResults() { + const { shipId: currentShipId, search } = shipDataStorage.getStore() + const shipResults = await searchShips({ search }) + return shipResults.ships.map((ship) => { + const href = [ + `/${ship.id}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h( + 'li', + { key: ship.name }, + h( + 'a', + { + href, + style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, + }, + h('img', { + 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/01.exercises/07.problem.module-graph/.gitignore b/exercises/03.client-components/02.problem.module-resolution/.gitignore similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/.gitignore rename to exercises/03.client-components/02.problem.module-resolution/.gitignore diff --git a/exercises/03.client-components/02.problem.module-resolution/README.mdx b/exercises/03.client-components/02.problem.module-resolution/README.mdx new file mode 100644 index 0000000..d964621 --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/README.mdx @@ -0,0 +1,37 @@ +# Module Resolution + + + +๐Ÿ‘จโ€๐Ÿ’ผ We've successfully converted any of our `'use client'` modules into special +modules which register themselves as client references. Now we need to help +`react-server-dom-esm` resolve these properly when generating the RSC payload +and resolve that to the correct URL for loading the client module in the +browser. + +On the server-side, we need to tell `renderToPipeableStream` how to convert the +full-path file URL of our client modules into relative URLs the browser can use. +This will also allow `renderToPipeableStream` to warn us if any client modules +are imported outside of the proper base directory. So you need to pass the path +to the base directory of our client modules as the second argument to +`renderToPipeableStream`: + +```js +renderToPipeableStream(h(App), moduleBasePath) +``` + +On the client-side, we need to tell `createFromFetch` how to convert the +relative path into a full URL for fetching the client module. + +```js +createFromFetch(promise, { moduleBaseURL }) +``` + + + Because `react-server-dom-esm` is the one performing the dynamic import, all + imports will be relative to that module. On the client, we're loading it from + [esm.sh](https://esm.sh), so we'll want to make sure we give the full URL to + our server including the origin. + + +Once you're finished with this, you may want to take a look at what the RSC +payload looks like on a page with a client component. diff --git a/exercises/03.client-components/02.problem.module-resolution/db/ship-api.js b/exercises/03.client-components/02.problem.module-resolution/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/03.client-components/02.problem.module-resolution/db/ships.json b/exercises/03.client-components/02.problem.module-resolution/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/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/03.client-components/02.problem.module-resolution/package.json b/exercises/03.client-components/02.problem.module-resolution/package.json new file mode 100644 index 0000000..3b70ff8 --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__03.client-components__sep__02.problem.module-resolution", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/03.client-components/02.problem.module-resolution/public/favicon.ico b/exercises/03.client-components/02.problem.module-resolution/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/03.client-components/02.problem.module-resolution/public/favicon.ico differ diff --git a/exercises/03.client-components/02.problem.module-resolution/public/favicon.svg b/exercises/03.client-components/02.problem.module-resolution/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/03.client-components/02.problem.module-resolution/public/iframe-sync.js b/exercises/03.client-components/02.problem.module-resolution/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/broken-ship.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/broken-ship.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/broken-ship.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/fallback-ship.png b/exercises/03.client-components/02.problem.module-resolution/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/fallback-ship.png rename to exercises/03.client-components/02.problem.module-resolution/public/img/fallback-ship.png diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/0268fc4817ad1.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/0268fc4817ad1.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/1ae7b4b92036b.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/1ae7b4b92036b.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/1ff1991efe029.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/1ff1991efe029.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/3ba8aa65ffe6c.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/441f7092a8d44.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/441f7092a8d44.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/5c13d8b28a14a.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/5c13d8b28a14a.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/627c497212456.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/627c497212456.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/670003aed3795.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/670003aed3795.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/6c86fca8b9086.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/6c86fca8b9086.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/6f375578ead88.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/6f375578ead88.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/ab267a5984523.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/ab267a5984523.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/b442531ea32b2.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/b442531ea32b2.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/bc4cbadf89bd3.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/bc4cbadf89bd3.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/cb03cc4e5717e.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/cb03cc4e5717e.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/cfd10fcd2de6c.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/cfd10fcd2de6c.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/d3b8aa65ffe6c.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/d486d48b82b81.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/d486d48b82b81.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/e92cefe4f6727.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/e92cefe4f6727.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/ec7a3f950f99f.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/ec7a3f950f99f.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/f3d9a88e1c234.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/f3d9a88e1c234.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/07.problem.module-graph/public/img/ships/fdc13cb488bf1.webp b/exercises/03.client-components/02.problem.module-resolution/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/img/ships/fdc13cb488bf1.webp rename to exercises/03.client-components/02.problem.module-resolution/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/03.client-components/02.problem.module-resolution/public/index.html b/exercises/03.client-components/02.problem.module-resolution/public/index.html new file mode 100644 index 0000000..1649718 --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/public/index.html @@ -0,0 +1,26 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/07.problem.module-graph/public/style.css b/exercises/03.client-components/02.problem.module-resolution/public/style.css similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/public/style.css rename to exercises/03.client-components/02.problem.module-resolution/public/style.css diff --git a/exercises/03.client-components/02.problem.module-resolution/server/app.js b/exercises/03.client-components/02.problem.module-resolution/server/app.js new file mode 100644 index 0000000..42d3c18 --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/server/app.js @@ -0,0 +1,79 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + // ๐Ÿจ create a moduleBasePath variable set to new URL('../ui', import.meta.url).href + // ๐Ÿจ pass the moduleBase path as a second argument to renderToPipeableStream + const { pipe } = renderToPipeableStream(h(App)) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/04.problem.async-components/server/async-storage.js b/exercises/03.client-components/02.problem.module-resolution/server/async-storage.js similarity index 100% rename from exercises/01.exercises/04.problem.async-components/server/async-storage.js rename to exercises/03.client-components/02.problem.module-resolution/server/async-storage.js diff --git a/exercises/01.exercises/09.problem.routing/server/register-rsc-loader.js b/exercises/03.client-components/02.problem.module-resolution/server/register-rsc-loader.js similarity index 100% rename from exercises/01.exercises/09.problem.routing/server/register-rsc-loader.js rename to exercises/03.client-components/02.problem.module-resolution/server/register-rsc-loader.js diff --git a/exercises/03.client-components/02.problem.module-resolution/server/rsc-loader.js b/exercises/03.client-components/02.problem.module-resolution/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/03.client-components/02.problem.module-resolution/tests/edit-text.test.js b/exercises/03.client-components/02.problem.module-resolution/tests/edit-text.test.js new file mode 100644 index 0000000..8a8205e --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/tests/edit-text.test.js @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test' +import { searchShips } from '../db/ship-api.js' + +test('should display the home page and perform search', async ({ page }) => { + const { + ships: [ship], + } = await searchShips({ search: 'hopper' }) + const newName = `${ship.name} ${Math.random().toString(16).slice(2, 5)}` + await page.goto(`/${ship.id}`) + + // Wait for the loading state to disappear + await page.waitForSelector('h2:has-text("Loading...")', { state: 'detached' }) + + // Ensure the ship name is visible + await expect(page.getByRole('heading', { name: ship.name })).toBeVisible() + // Find and click the edit button + await page.getByRole('button', { name: ship.name }).click() + + // Check if the input is focused + await expect(page.getByRole('textbox', { name: 'Ship Name' })).toBeFocused() + + // Change the value of the input + await page.getByRole('textbox', { name: 'Ship Name' }).fill(newName) + + // Press Enter + await page.keyboard.press('Enter') + + // Check if the button is back + await expect(page.getByRole('button', { name: newName })).toBeVisible() +}) diff --git a/exercises/03.client-components/02.problem.module-resolution/tests/playwright.config.js b/exercises/03.client-components/02.problem.module-resolution/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/03.client-components/02.problem.module-resolution/tests/smoke.test.js b/exercises/03.client-components/02.problem.module-resolution/tests/smoke.test.js new file mode 100644 index 0000000..c1ac38f --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/tests/smoke.test.js @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + await filterInput.press('Enter') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + await page.waitForLoadState('networkidle') + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/03.client-components/02.problem.module-resolution/ui/app.js b/exercises/03.client-components/02.problem.module-resolution/ui/app.js new file mode 100644 index 0000000..7df8dc7 --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/ui/app.js @@ -0,0 +1,43 @@ +import { Fragment, Suspense, createElement as h } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ShipDetails, ShipFallback } from './ship-details.js' +import { SearchResults, SearchResultsFallback } from './ship-search-results.js' + +export function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h( + Fragment, + null, + h( + 'form', + {}, + h('input', { + name: 'search', + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + autoFocus: true, + }), + ), + h( + 'ul', + null, + h(Suspense, { fallback: h(SearchResultsFallback) }, h(SearchResults)), + ), + ), + ), + h( + 'div', + { className: 'details' }, + shipId + ? h(Suspense, { fallback: h(ShipFallback) }, h(ShipDetails)) + : h('p', null, 'Select a ship from the list to see details'), + ), + ) +} diff --git a/exercises/03.client-components/02.problem.module-resolution/ui/edit-text.js b/exercises/03.client-components/02.problem.module-resolution/ui/edit-text.js new file mode 100644 index 0000000..b30c19f --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/ui/edit-text.js @@ -0,0 +1,84 @@ +'use client' + +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + null, + edit + ? h( + 'form', + { + onSubmit: (event) => { + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + ) +} diff --git a/exercises/01.exercises/10.problem.actions/src/img-utils.js b/exercises/03.client-components/02.problem.module-resolution/ui/img-utils.js similarity index 100% rename from exercises/01.exercises/10.problem.actions/src/img-utils.js rename to exercises/03.client-components/02.problem.module-resolution/ui/img-utils.js diff --git a/exercises/03.client-components/02.problem.module-resolution/ui/index.js b/exercises/03.client-components/02.problem.module-resolution/ui/index.js new file mode 100644 index 0000000..34870b5 --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/ui/index.js @@ -0,0 +1,37 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +import { createFromFetch } from 'react-server-dom-esm/client' +import { shipFallbackSrc } from './img-utils.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +const initialLocation = getGlobalLocation() +const initialContentFetchPromise = fetch(`/rsc${initialLocation}`) +const initialContentPromise = createFromFetch(initialContentFetchPromise, { + // ๐Ÿจ add a moduleBaseURL option here set to `${window.location.origin}/ui` +}) + +function Root() { + const content = use(initialContentPromise) + return content +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h(Root), + ), + ), + ) +}) diff --git a/exercises/03.client-components/02.problem.module-resolution/ui/ship-details.js b/exercises/03.client-components/02.problem.module-resolution/ui/ship-details.js new file mode 100644 index 0000000..c133dff --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/ui/ship-details.js @@ -0,0 +1,97 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { EditableText } from './edit-text.js' +import { getImageUrlForShip } from './img-utils.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('img', { src: shipImgSrc, alt: ship.name }), + ), + h( + 'section', + null, + h( + 'h2', + null, + h(EditableText, { + key: shipId, + shipId, + initialValue: 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('img', { + 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)')), + ), + ), + ), + ), + ) +} diff --git a/exercises/03.client-components/02.problem.module-resolution/ui/ship-search-results.js b/exercises/03.client-components/02.problem.module-resolution/ui/ship-search-results.js new file mode 100644 index 0000000..69be0af --- /dev/null +++ b/exercises/03.client-components/02.problem.module-resolution/ui/ship-search-results.js @@ -0,0 +1,50 @@ +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' + +export async function SearchResults() { + const { shipId: currentShipId, search } = shipDataStorage.getStore() + const shipResults = await searchShips({ search }) + return shipResults.ships.map((ship) => { + const href = [ + `/${ship.id}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h( + 'li', + { key: ship.name }, + h( + 'a', + { + href, + style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, + }, + h('img', { + 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/01.exercises/07.solution.module-graph/.gitignore b/exercises/03.client-components/02.solution.module-resolution/.gitignore similarity index 100% rename from exercises/01.exercises/07.solution.module-graph/.gitignore rename to exercises/03.client-components/02.solution.module-resolution/.gitignore diff --git a/exercises/03.client-components/02.solution.module-resolution/README.mdx b/exercises/03.client-components/02.solution.module-resolution/README.mdx new file mode 100644 index 0000000..58e4de4 --- /dev/null +++ b/exercises/03.client-components/02.solution.module-resolution/README.mdx @@ -0,0 +1,50 @@ +# Module Resolution + + + +๐Ÿ‘จโ€๐Ÿ’ผ Great! Now our server is all configured to create `'use client'` module +references to properly separate our client and server code. And our client is +configured to resolve those references to the client code necessary to run in +the browser and make the components stateful and interactive! + +If you check out the RSC payload now, you'll notice that the client component +is now referenced in the RSC payload: + +{/* prettier-ignore */} +```json nonumber nolang lines=15-16 +2:"$Sreact.suspense" +1:{"name":"App","env":"Server","owner":null} +0:D"$1" +4:{"name":"SearchResultsFallback","env":"Server","owner":"$1"} +3:D"$4" +3:[["$","li","0",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"},"$4"],"... loading"]},"$4"]},"$4"],["$","li","1",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"},"$4"],"... loading"]},"$4"]},"$4"],["$","li","2",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"},"$4"],"... loading"]},"$4"]},"$4"],["$","li","3",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"},"$4"],"... loading"]},"$4"]},"$4"],["$","li","4",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"},"$4"],"... loading"]},"$4"]},"$4"],["$","li","5",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"},"$4"],"... loading"]},"$4"]},"$4"],["$","li","6",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"},"$4"],"... loading"]},"$4"]},"$4"],["$","li","7",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"},"$4"],"... loading"]},"$4"]},"$4"],["$","li","8",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"},"$4"],"... loading"]},"$4"]},"$4"],["$","li","9",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"},"$4"],"... loading"]},"$4"]},"$4"],["$","li","10",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"},"$4"],"... loading"]},"$4"]},"$4"],["$","li","11",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"},"$4"],"... loading"]},"$4"]},"$4"]] +6:{"name":"SearchResults","env":"Server","owner":"$1"} +5:D"$6" +8:{"name":"ShipFallback","env":"Server","owner":"$1"} +7:D"$8" +7:["$","div",null,{"className":"ship-info","children":[["$","div",null,{"className":"ship-info__img-wrapper","children":["$","img",null,{"src":"/img/ships/ec7a3f950f99f.webp?size=200","alt":"ec7a3f950f99f"},"$8"]},"$8"],["$","section",null,{"children":["$","h2",null,{"children":"Loading..."},"$8"]},"$8"],["$","div",null,{"children":["Top Speed: XX"," ",["$","small",null,{"children":"lyh"},"$8"]]},"$8"],["$","section",null,{"children":["$","ul",null,{"children":[["$","li","0",{"children":[["$","label",null,{"children":"loading"},"$8"],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"},"$8"]]},"$8"]]},"$8"],["$","li","1",{"children":[["$","label",null,{"children":"loading"},"$8"],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"},"$8"]]},"$8"]]},"$8"],["$","li","2",{"children":[["$","label",null,{"children":"loading"},"$8"],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"},"$8"]]},"$8"]]},"$8"]]},"$8"]},"$8"]]},"$8"] +a:{"name":"ShipDetails","env":"Server","owner":"$1"} +9:D"$a" +0:["$","div",null,{"className":"app","children":[["$","div",null,{"className":"search","children":[["$","form",null,{"children":["$","input",null,{"name":"search","placeholder":"Filter ships...","type":"search","defaultValue":"","autoFocus":true},"$1"]},"$1"],["$","ul",null,{"children":["$","$2",null,{"fallback":"$3","children":"$L5"},"$1"]},"$1"]]},"$1"],["$","div",null,{"className":"details","children":["$","$2",null,{"fallback":"$7","children":"$L9"},"$1"]},"$1"]]},"$1"] +b:I["/edit-text.js","EditableText"] +9:["$","div",null,{"className":"ship-info","children":[["$","div",null,{"className":"ship-info__img-wrapper","children":["$","img",null,{"src":"/img/ships/ec7a3f950f99f.webp?size=200","alt":"Scout Ship"},null]},null],["$","section",null,{"children":["$","h2",null,{"children":["$","$Lb","ec7a3f950f99f",{"shipId":"ec7a3f950f99f","initialValue":"Scout Ship"},null]},null]},null],["$","div",null,{"children":["Top Speed: ",11," ",["$","small",null,{"children":"lyh"},null]]},null],["$","section",null,{"children":["$","p",null,{"children":"NOTE: This ship is not equipped with any weapons."},null]},null]]},null] +5:[["$","li","Infinity Drifter",{"children":["$","a",null,{"href":"/bc4cbadf89bd3","style":{"fontWeight":"normal"},"children":[["$","img",null,{"src":"/img/ships/bc4cbadf89bd3.webp?size=20","alt":"Infinity Drifter"},null],"Infinity Drifter"]},null]},null],["$","li","Star Hopper",{"children":["$","a",null,{"href":"/3ba8aa65ffe6c","style":{"fontWeight":"normal"},"children":[["$","img",null,{"src":"/img/ships/3ba8aa65ffe6c.webp?size=20","alt":"Star Hopper"},null],"Star Hopper"]},null]},null],["$","li","Galaxy Cruiser",{"children":["$","a",null,{"href":"/ab267a5984523","style":{"fontWeight":"normal"},"children":[["$","img",null,{"src":"/img/ships/ab267a5984523.webp?size=20","alt":"Galaxy Cruiser"},null],"Galaxy Cruiser"]},null]},null],["$","li","Planet Hopper",{"children":["$","a",null,{"href":"/d3b8aa65ffe6c","style":{"fontWeight":"normal"},"children":[["$","img",null,{"src":"/img/ships/d3b8aa65ffe6c.webp?size=20","alt":"Planet Hopper"},null],"Planet Hopper"]},null]},null],["$","li","Space Taxi",{"children":["$","a",null,{"href":"/1ff1991efe029","style":{"fontWeight":"normal"},"children":[["$","img",null,{"src":"/img/ships/1ff1991efe029.webp?size=20","alt":"Space Taxi"},null],"Space Taxi"]},null]},null],["$","li","Star Destroyer",{"children":["$","a",null,{"href":"/f3d9a88e1c234","style":{"fontWeight":"normal"},"children":[["$","img",null,{"src":"/img/ships/f3d9a88e1c234.webp?size=20","alt":"Star Destroyer"},null],"Star Destroyer"]},null]},null],["$","li","Interceptor",{"children":["$","a",null,{"href":"/cb03cc4e5717e","style":{"fontWeight":"normal"},"children":[["$","img",null,{"src":"/img/ships/cb03cc4e5717e.webp?size=20","alt":"Interceptor"},null],"Interceptor"]},null]},null],["$","li","Stealth Cruiser",{"children":["$","a",null,{"href":"/6c86fca8b9086","style":{"fontWeight":"normal"},"children":[["$","img",null,{"src":"/img/ships/6c86fca8b9086.webp?size=20","alt":"Stealth Cruiser"},null],"Stealth Cruiser"]},null]},null],["$","li","Battleship",{"children":["$","a",null,{"href":"/fdc13cb488bf1","style":{"fontWeight":"normal"},"children":[["$","img",null,{"src":"/img/ships/fdc13cb488bf1.webp?size=20","alt":"Battleship"},null],"Battleship"]},null]},null],["$","li","Dreadnought",{"children":["$","a",null,{"href":"/d486d48b82b81","style":{"fontWeight":"normal"},"children":[["$","img",null,{"src":"/img/ships/d486d48b82b81.webp?size=20","alt":"Dreadnought"},null],"Dreadnought"]},null]},null],["$","li","Cruiser",{"children":["$","a",null,{"href":"/cfd10fcd2de6c","style":{"fontWeight":"normal"},"children":[["$","img",null,{"src":"/img/ships/cfd10fcd2de6c.webp?size=20","alt":"Cruiser"},null],"Cruiser"]},null]},null],["$","li","Frigate",{"children":["$","a",null,{"href":"/e92cefe4f6727","style":{"fontWeight":"normal"},"children":[["$","img",null,{"src":"/img/ships/e92cefe4f6727.webp?size=20","alt":"Frigate"},null],"Frigate"]},null]},null],["$","li","Scout Ship",{"children":["$","a",null,{"href":"/ec7a3f950f99f","style":{"fontWeight":"bold"},"children":[["$","img",null,{"src":"/img/ships/ec7a3f950f99f.webp?size=20","alt":"Scout Ship"},null],"Scout Ship"]},null]},null]] +``` + +What's so cool about this is it allows us to have composition across the wire. +We can compose our client and server code together! + +A common question at this point is "do I need to add `'use client'` to every +module I want in the client." The answer is no! You only need to add `'use +client'` to the top-level module that you want to run in the client. Think of +`'use client'` as a ` + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/07.solution.module-graph/public/style.css b/exercises/03.client-components/02.solution.module-resolution/public/style.css similarity index 100% rename from exercises/01.exercises/07.solution.module-graph/public/style.css rename to exercises/03.client-components/02.solution.module-resolution/public/style.css diff --git a/exercises/03.client-components/02.solution.module-resolution/server/app.js b/exercises/03.client-components/02.solution.module-resolution/server/app.js new file mode 100644 index 0000000..c311fef --- /dev/null +++ b/exercises/03.client-components/02.solution.module-resolution/server/app.js @@ -0,0 +1,78 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const moduleBasePath = new URL('../ui', import.meta.url).href + const { pipe } = renderToPipeableStream(h(App), moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/04.solution.async-components/server/async-storage.js b/exercises/03.client-components/02.solution.module-resolution/server/async-storage.js similarity index 100% rename from exercises/01.exercises/04.solution.async-components/server/async-storage.js rename to exercises/03.client-components/02.solution.module-resolution/server/async-storage.js diff --git a/exercises/01.exercises/09.solution.routing/server/register-rsc-loader.js b/exercises/03.client-components/02.solution.module-resolution/server/register-rsc-loader.js similarity index 100% rename from exercises/01.exercises/09.solution.routing/server/register-rsc-loader.js rename to exercises/03.client-components/02.solution.module-resolution/server/register-rsc-loader.js diff --git a/exercises/03.client-components/02.solution.module-resolution/server/rsc-loader.js b/exercises/03.client-components/02.solution.module-resolution/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/03.client-components/02.solution.module-resolution/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/03.client-components/02.solution.module-resolution/tests/edit-text.test.js b/exercises/03.client-components/02.solution.module-resolution/tests/edit-text.test.js new file mode 100644 index 0000000..8a8205e --- /dev/null +++ b/exercises/03.client-components/02.solution.module-resolution/tests/edit-text.test.js @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test' +import { searchShips } from '../db/ship-api.js' + +test('should display the home page and perform search', async ({ page }) => { + const { + ships: [ship], + } = await searchShips({ search: 'hopper' }) + const newName = `${ship.name} ${Math.random().toString(16).slice(2, 5)}` + await page.goto(`/${ship.id}`) + + // Wait for the loading state to disappear + await page.waitForSelector('h2:has-text("Loading...")', { state: 'detached' }) + + // Ensure the ship name is visible + await expect(page.getByRole('heading', { name: ship.name })).toBeVisible() + // Find and click the edit button + await page.getByRole('button', { name: ship.name }).click() + + // Check if the input is focused + await expect(page.getByRole('textbox', { name: 'Ship Name' })).toBeFocused() + + // Change the value of the input + await page.getByRole('textbox', { name: 'Ship Name' }).fill(newName) + + // Press Enter + await page.keyboard.press('Enter') + + // Check if the button is back + await expect(page.getByRole('button', { name: newName })).toBeVisible() +}) diff --git a/exercises/03.client-components/02.solution.module-resolution/tests/playwright.config.js b/exercises/03.client-components/02.solution.module-resolution/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/03.client-components/02.solution.module-resolution/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/03.client-components/02.solution.module-resolution/tests/smoke.test.js b/exercises/03.client-components/02.solution.module-resolution/tests/smoke.test.js new file mode 100644 index 0000000..c1ac38f --- /dev/null +++ b/exercises/03.client-components/02.solution.module-resolution/tests/smoke.test.js @@ -0,0 +1,42 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + await filterInput.press('Enter') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + await page.waitForLoadState('networkidle') + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/03.client-components/02.solution.module-resolution/ui/app.js b/exercises/03.client-components/02.solution.module-resolution/ui/app.js new file mode 100644 index 0000000..7df8dc7 --- /dev/null +++ b/exercises/03.client-components/02.solution.module-resolution/ui/app.js @@ -0,0 +1,43 @@ +import { Fragment, Suspense, createElement as h } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ShipDetails, ShipFallback } from './ship-details.js' +import { SearchResults, SearchResultsFallback } from './ship-search-results.js' + +export function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h( + Fragment, + null, + h( + 'form', + {}, + h('input', { + name: 'search', + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + autoFocus: true, + }), + ), + h( + 'ul', + null, + h(Suspense, { fallback: h(SearchResultsFallback) }, h(SearchResults)), + ), + ), + ), + h( + 'div', + { className: 'details' }, + shipId + ? h(Suspense, { fallback: h(ShipFallback) }, h(ShipDetails)) + : h('p', null, 'Select a ship from the list to see details'), + ), + ) +} diff --git a/exercises/03.client-components/02.solution.module-resolution/ui/edit-text.js b/exercises/03.client-components/02.solution.module-resolution/ui/edit-text.js new file mode 100644 index 0000000..b30c19f --- /dev/null +++ b/exercises/03.client-components/02.solution.module-resolution/ui/edit-text.js @@ -0,0 +1,84 @@ +'use client' + +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + null, + edit + ? h( + 'form', + { + onSubmit: (event) => { + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + ) +} diff --git a/exercises/01.exercises/10.solution.actions/src/img-utils.js b/exercises/03.client-components/02.solution.module-resolution/ui/img-utils.js similarity index 100% rename from exercises/01.exercises/10.solution.actions/src/img-utils.js rename to exercises/03.client-components/02.solution.module-resolution/ui/img-utils.js diff --git a/exercises/03.client-components/02.solution.module-resolution/ui/index.js b/exercises/03.client-components/02.solution.module-resolution/ui/index.js new file mode 100644 index 0000000..9fa07f2 --- /dev/null +++ b/exercises/03.client-components/02.solution.module-resolution/ui/index.js @@ -0,0 +1,37 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +import { createFromFetch } from 'react-server-dom-esm/client' +import { shipFallbackSrc } from './img-utils.js' + +const getGlobalLocation = () => + window.location.pathname + window.location.search + +const initialLocation = getGlobalLocation() +const initialContentFetchPromise = fetch(`/rsc${initialLocation}`) +const initialContentPromise = createFromFetch(initialContentFetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, +}) + +function Root() { + const content = use(initialContentPromise) + return content +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + h( + Suspense, + { + fallback: h('img', { + style: { maxWidth: 400 }, + src: shipFallbackSrc, + }), + }, + h(Root), + ), + ), + ) +}) diff --git a/exercises/03.client-components/02.solution.module-resolution/ui/ship-details.js b/exercises/03.client-components/02.solution.module-resolution/ui/ship-details.js new file mode 100644 index 0000000..c133dff --- /dev/null +++ b/exercises/03.client-components/02.solution.module-resolution/ui/ship-details.js @@ -0,0 +1,97 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { EditableText } from './edit-text.js' +import { getImageUrlForShip } from './img-utils.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('img', { src: shipImgSrc, alt: ship.name }), + ), + h( + 'section', + null, + h( + 'h2', + null, + h(EditableText, { + key: shipId, + shipId, + initialValue: 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('img', { + 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)')), + ), + ), + ), + ), + ) +} diff --git a/exercises/03.client-components/02.solution.module-resolution/ui/ship-search-results.js b/exercises/03.client-components/02.solution.module-resolution/ui/ship-search-results.js new file mode 100644 index 0000000..69be0af --- /dev/null +++ b/exercises/03.client-components/02.solution.module-resolution/ui/ship-search-results.js @@ -0,0 +1,50 @@ +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' + +export async function SearchResults() { + const { shipId: currentShipId, search } = shipDataStorage.getStore() + const shipResults = await searchShips({ search }) + return shipResults.ships.map((ship) => { + const href = [ + `/${ship.id}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h( + 'li', + { key: ship.name }, + h( + 'a', + { + href, + style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, + }, + h('img', { + 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/03.client-components/FINISHED.mdx b/exercises/03.client-components/FINISHED.mdx new file mode 100644 index 0000000..377ffdd --- /dev/null +++ b/exercises/03.client-components/FINISHED.mdx @@ -0,0 +1,6 @@ +# Client Components + + + +๐Ÿ‘จโ€๐Ÿ’ผ Well done! Now you understand how React Server Components give us composition +from the server to the client. Great job! ๐ŸŽ‰ diff --git a/exercises/03.client-components/README.mdx b/exercises/03.client-components/README.mdx new file mode 100644 index 0000000..eedfd22 --- /dev/null +++ b/exercises/03.client-components/README.mdx @@ -0,0 +1,264 @@ +# Client Components + + + +Rendering things on the server works well. We can even click on links and +because we have state in the URL (whether in URL segment params or search query +params), our application will update as the user navigates around. We could even +make our server handle form submissions if we wanted to as well. Doing this, +we'd be able to completely avoid client-side JavaScript. + +But we're not in the business of avoiding client-side JavaScript. We're in the +business of excellent user experiences. And modern user experience demands +client-side JavaScript. But we want to keep the composibility of React +components in the client as well as the server. Especially because our RSC setup +enables mixing server-rendered components with client-rendered components. + +## The Divide + +Server components should be used for things that come from the server and don't +have client-side interactivity. For example, let's say you're building a todo +list. The list of items can be rendered by the server. And the button to mark +them as complete/incomplete can even be rendered by the server as well. + +However, if you want to implement optimistic UI for the checkbox to make the +user experience feel faster, you're going to need to manage state on the client +to simulate the update optimistically. This is a good example where you'd need +a client component. + +Another example would be a combobox. You need to manage state for the input +value, the filter, the selected item, etc. + +The great thing is that client and server components can be intermixed. But +there's an important caveat to how they're mixed as a result of the way server +components and client components interact. + +## Caveats + +There are some interesting caveats to consider when mixing client and server +components. The biggest one is the fact that server components can render client +components, but client components can't render server components. + +Here's a simple example to explain why: + +```jsx +'use client' +import { ServerComponent } from './server-component.js' + +function ClientComponent() { + // some client stuff + return ( +
+ {/* some other stuff ...*/} + +
+ ) +} +``` + +What happens when the `ClientComponent` renders? How would the client know +what the server component would render? By definition, the server component +code is not in the client, so it can't even have a reference to the server. + +In fact, that `import` for the `./server-component.js` module would not even +work in the client. That module could have secret environment variables or a +database connection etc. + +So no, you cannot `import` server modules in client modules for safety reasons. + +However, you can do the opposite. Client modules can be imported into server +modules. So if you need the structure of the example above, you would simply +change the structure: + +```jsx +'use client' + +function ClientComponent({ serverUI }) { + // some client stuff + return ( +
+ {/* some other stuff ...*/} + {serverUI} +
+ ) +} +``` + +And then a parent server component would render the client component as well as +the server component like so: + +```jsx +import { ClientComponent } from './client-component.js' +import { ServerComponent } from './server-component.js' + +function App() { + return } /> +} +``` + +This composition is safe because server modules can only be imported by other +server modules, and client modules (which by definition are safe to send to the +client) are imported by both server and client modules. + +## Client References + +There's another caveat to this and that is: what do we do with the client +components in the serialized JSX? Remember that there are two ways to get UI to +React to render in the browser: + +1. Data + Components in the client +2. RSC Payload + +So when we're generating the RSC payload, should we include the client component +code in the payload? If we did, then we would be duplicating the client +component code in the payload and in the client bundle. This would be a waste +of space. More than that, if the client component has already rendered on the +client and has state changes already, then what do we do if we get an updated +RSC payload? Blow away those state changes? + +No, we can't include client components in the RSC payload. So instead, the RSC +payload will include a placeholder for the client component. This placeholder +is a reference to the client component and the RSC payload will include +information on how to retrieve the module for the client component. + +Let's take another look at an example RSC payload: + +```json nonumber +1:D{"name":"App","env":"Server"} +0:{"returnValue":null,"root":"$L1"} +2:I["/ship-search.js","ShipSearch"] +5:I["/ship-details-pending.js","ShipDetailsPendingTransition"] +6:I["/error-boundary.js","ErrorBoundary"] +8:"$Sreact.suspense" +a:I["/img.js","ShipImg"] +3:D{"name":"SearchResults","env":"Server"} +4:D{"name":"SearchResultsFallback","env":"Server"} +4:[["$","li","0",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","1",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","2",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","3",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","4",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","5",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","6",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","7",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","8",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","9",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","10",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}],["$","li","11",{"children":["$","a",null,{"href":"#","children":[["$","img",null,{"src":"/img/fallback-ship.png","alt":"loading"}],"... loading"]}]}]] +7:D{"name":"ShipError","env":"Server"} +7:["$","div",null,{"className":"ship-info","children":[["$","div",null,{"className":"ship-info__img-wrapper","children":["$","img",null,{"src":"/img/broken-ship.webp","alt":"broken ship"}]}],["$","section",null,{"children":["$","h2",null,{"children":"There was an error"}]}],["$","section",null,{"children":["There was an error loading \"","0268fc4817ad1","\""]}]]}] +9:D{"name":"ShipFallback","env":"Server"} +9:["$","div",null,{"className":"ship-info","children":[["$","div",null,{"className":"ship-info__img-wrapper","children":["$","$La",null,{"src":"/img/ships/0268fc4817ad1.webp?size=200","alt":"0268fc4817ad1"}]}],["$","section",null,{"children":["$","h2",null,{"children":"Loading..."}]}],["$","div",null,{"children":["Top Speed: XX"," ",["$","small",null,{"children":"lyh"}]]}],["$","section",null,{"children":["$","ul",null,{"children":[["$","li","0",{"children":[["$","label",null,{"children":"loading"}],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"}]]}]]}],["$","li","1",{"children":[["$","label",null,{"children":"loading"}],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"}]]}]]}],["$","li","2",{"children":[["$","label",null,{"children":"loading"}],":"," ",["$","span",null,{"children":["XX ",["$","small",null,{"children":"(loading)"}]]}]]}]]}]}]]}] +b:D{"name":"ShipDetails","env":"Server"} +1:["$","div",null,{"className":"app","children":[["$","div",null,{"className":"search","children":["$","$L2",null,{"search":"m","results":"$L3","fallback":"$4"}]}],["$","$L5",null,{"children":["$","$L6",null,{"fallback":"$7","children":["$","$8",null,{"fallback":"$9","children":"$Lb"}]}]}]]}] +c:I["/ship-search.js","SelectShipLink"] +3:[["$","li","Bomber",{"children":["$","$Lc",null,{"shipId":"5c13d8b28a14a","highlight":false,"children":[["$","$La",null,{"src":"/img/ships/5c13d8b28a14a.webp?size=20","alt":"Bomber"}],"Bomber"]}]}],["$","li","Diplomatic Vessel",{"children":["$","$Lc",null,{"shipId":"6f375578ead88","highlight":false,"children":[["$","$La",null,{"src":"/img/ships/6f375578ead88.webp?size=20","alt":"Diplomatic Vessel"}],"Diplomatic Vessel"]}]}],["$","li","Mining Ship",{"children":["$","$Lc",null,{"shipId":"627c497212456","highlight":false,"children":[["$","$La",null,{"src":"/img/ships/627c497212456.webp?size=20","alt":"Mining Ship"}],"Mining Ship"]}]}],["$","li","Medical Ship",{"children":["$","$Lc",null,{"shipId":"0268fc4817ad1","highlight":true,"children":[["$","$La",null,{"src":"/img/ships/0268fc4817ad1.webp?size=20","alt":"Medical Ship"}],"Medical Ship"]}]}]] +b:["$","div",null,{"className":"ship-info","children":[["$","div",null,{"className":"ship-info__img-wrapper","children":["$","$La",null,{"src":"/img/ships/0268fc4817ad1.webp?size=200","alt":"Medical Ship"}]}],["$","section",null,{"children":["$","h2",null,{"children":"Medical Ship"}]}],["$","div",null,{"children":["Top Speed: ",2," ",["$","small",null,{"children":"lyh"}]]}],["$","section",null,{"children":["$","p",null,{"children":"NOTE: This ship is not equipped with any weapons."}]}]]}] +``` + +We're not here to understand this format deeply (and it's possibly subject to +change anyway). So let's cut out some of the bits in the payload to focus a bit +and I'll format it to make it easier to read: + +```json nonumber +... +2:I["/ship-search.js","ShipSearch"] +// ... +1:[ + "$", + "div", + null, + { + "className": "app", + "children": [ + [ + "$", + "div", + null, + { + "className": "search", + "children": [ + "$", + "$L2", + null, + { "search": "m", "results": "$L3", "fallback": "$4" } + ] + } + ], + [ + // ... + ] + ] + } +] +... +``` + +Notice there's an entry for the `I` type with the id of `2` and that's +referenced in the `1` entry by `$L2`. + +So when React is processing the RSC payload, it sees that `$L2` is a reference, +it looks up that reference, sees that it's a client component and knows which +module to find it in. It imports `ShipSearch` from `/ship-search.js` and renders +it in that place. + +This is how React avoids duplicating client component code in the RSC payload. + +## `use client` + +So then the question is, how do we avoid duplicating client component code in +the server when we're importing those modules and rendering them in our server +components? + +The answer is the bundler you're using. When you're creating the RSC bundler, +you can tell it to replace certain modules with the reference to the client +module. And you tell it using the `'use client'`. + +We can do this without a bundler by registering a custom +[Node.js loader](https://nodejs.org/api/module.html#customization-hooks). +Luckily, for us, `react-server-dom-esm` exports a loader for us to use and as +this isn't a workshop about Node.js loaders, you won't be required to configure +it yourself. But it's important that you understand what it's doing. So we'll +have you toss a few `console.logs` around until you get it. + +## Importing Client Components + +One final thing you're going to need to understand about client components is +how they're loaded. You're not responsible for loading them yourself. + +We hand off the RSC payload to `react-server-dom-esm/client` which will take +care of loading the client components for you. All we have to do is tell it +where to get the modules. + +You'll recall from the RSC format above that the client modules look like this: + +```json nonumber +2:I["/ship-search.js","ShipSearch"] +``` + +The first item in the array is the path to the module and the second item is +the name of the `export` in module. The full path to that module will be +something like +`file:///Users/kentcdodds/code/epicweb-dev/react-server-components/playground/ui/ship-search.js`. + +So when we call `renderToPipeableStream` with the RSC payload, we'll also provide +`react-server-dom-esm/server` with a `moduleBasePath` option. This allows you to +control where the client modules can be loaded from the file system and used to +shorten the paths in the RSC payload: + +```js +const moduleBasePath = new URL('../ui', import.meta.url).href +// moduleBasePath is something like `file:///Users/kentcdodds/code/epicweb-dev/react-server-components/playground/ui` +const { pipe } = renderToPipeableStream(payload, moduleBasePath) +``` + +With the full file path removed, we're left with the path to the module relative +to our `ui` directory. Our hono.js server is configured to serve any file in +`ui` via `/ui`. + +Because `react-server-dom-esm/client` is responsible for loading that module for +us, we need to tell it the base URL to load modules from via an option called +`moduleBaseURL`: + +```js +createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, +}) +``` + +Then when `react-server-dom-esm/client` needs to load a module, it will fetch +the module from `${window.location.origin}/ui/ship-search.js` and `import` +the `ShipSearch` `export` from it. + +๐Ÿ“œ Relevant Docs: + +- [`use-client`](https://react.dev/reference/react/use-client) diff --git a/exercises/01.exercises/08.problem.hydrate/.gitignore b/exercises/04.router/01.problem.router/.gitignore similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/.gitignore rename to exercises/04.router/01.problem.router/.gitignore diff --git a/exercises/04.router/01.problem.router/README.mdx b/exercises/04.router/01.problem.router/README.mdx new file mode 100644 index 0000000..bbec238 --- /dev/null +++ b/exercises/04.router/01.problem.router/README.mdx @@ -0,0 +1,51 @@ +# Client Router + + + +๐Ÿ‘จโ€๐Ÿ’ผ In our file, we have our search form +and the list of results which have links. The form and the list of results +technically all works, but triggers full page refreshes which we'd love to +avoid. Now that we have client components, we've made this a client module with +`'use client'` so we can use things like event handlers to prevent the default +behavior and update the UI without a full page refresh. + +๐Ÿงโ€โ™‚๏ธ I created which is just a couple +utilities and a context for managing the state in a router along with a +`useRouter()` hook for accessing the context. You'll use these utilities to +navigate the user to the next destination as the user searches and selects +ships. + +๐Ÿ‘จโ€๐Ÿ’ผ Great, thank you Kellie! So what we need you to do is update +our to render the `RouterContext` +with the right values (you'll be implementing the `navigate` function) and then +update the module to use the +`useRouter()` hook to navigate the user to the ship details page when they click +on a ship link. + +๐Ÿงโ€โ™‚๏ธ Here's a tip on the `mergeLocationState` utility you're going to need: + +```js +import { mergeLocationState } from './router.js' + +const location = '/abc123?search=starship' + +const updatedSearch = mergeLocationState(location, { search: 'rocket' }) +// ^ '/abc123?search=rocket' + +const updatedShipId = mergeLocationState(location, { shipId: 'zxy987' }) +// ^ '/zxy987?search=starship' +``` + +Once you have the updated location, you can pass that to the `navigate` function +you're gonna write. + +Another tip, you're going to be using the +[`window.history`](https://developer.mozilla.org/en-US/docs/Web/API/History) API +to update the URL in the browser without triggering a full-page reload. You'll +want to use the `pushState` method when the user selects a ship and +`replaceState` when the user types in the search. Feel free to read the docs to +dive into the differences between those, but this means your `navigate` function +will need to accept an option to determine if it should push or replace the +state. + +Good luck! diff --git a/exercises/04.router/01.problem.router/db/ship-api.js b/exercises/04.router/01.problem.router/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/04.router/01.problem.router/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/04.router/01.problem.router/db/ships.json b/exercises/04.router/01.problem.router/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/04.router/01.problem.router/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/04.router/01.problem.router/package.json b/exercises/04.router/01.problem.router/package.json new file mode 100644 index 0000000..fb189c6 --- /dev/null +++ b/exercises/04.router/01.problem.router/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__04.router__sep__01.problem.router", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/04.router/01.problem.router/public/favicon.ico b/exercises/04.router/01.problem.router/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/04.router/01.problem.router/public/favicon.ico differ diff --git a/exercises/04.router/01.problem.router/public/favicon.svg b/exercises/04.router/01.problem.router/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/04.router/01.problem.router/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/04.router/01.problem.router/public/iframe-sync.js b/exercises/04.router/01.problem.router/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/04.router/01.problem.router/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/broken-ship.webp b/exercises/04.router/01.problem.router/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/broken-ship.webp rename to exercises/04.router/01.problem.router/public/img/broken-ship.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/fallback-ship.png b/exercises/04.router/01.problem.router/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/fallback-ship.png rename to exercises/04.router/01.problem.router/public/img/fallback-ship.png diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/0268fc4817ad1.webp b/exercises/04.router/01.problem.router/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/0268fc4817ad1.webp rename to exercises/04.router/01.problem.router/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/1ae7b4b92036b.webp b/exercises/04.router/01.problem.router/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/1ae7b4b92036b.webp rename to exercises/04.router/01.problem.router/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/1ff1991efe029.webp b/exercises/04.router/01.problem.router/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/1ff1991efe029.webp rename to exercises/04.router/01.problem.router/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/3ba8aa65ffe6c.webp b/exercises/04.router/01.problem.router/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/04.router/01.problem.router/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/441f7092a8d44.webp b/exercises/04.router/01.problem.router/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/441f7092a8d44.webp rename to exercises/04.router/01.problem.router/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/5c13d8b28a14a.webp b/exercises/04.router/01.problem.router/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/5c13d8b28a14a.webp rename to exercises/04.router/01.problem.router/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/627c497212456.webp b/exercises/04.router/01.problem.router/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/627c497212456.webp rename to exercises/04.router/01.problem.router/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/670003aed3795.webp b/exercises/04.router/01.problem.router/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/670003aed3795.webp rename to exercises/04.router/01.problem.router/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/6c86fca8b9086.webp b/exercises/04.router/01.problem.router/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/6c86fca8b9086.webp rename to exercises/04.router/01.problem.router/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/6f375578ead88.webp b/exercises/04.router/01.problem.router/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/6f375578ead88.webp rename to exercises/04.router/01.problem.router/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/ab267a5984523.webp b/exercises/04.router/01.problem.router/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/ab267a5984523.webp rename to exercises/04.router/01.problem.router/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/b442531ea32b2.webp b/exercises/04.router/01.problem.router/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/b442531ea32b2.webp rename to exercises/04.router/01.problem.router/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/bc4cbadf89bd3.webp b/exercises/04.router/01.problem.router/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/bc4cbadf89bd3.webp rename to exercises/04.router/01.problem.router/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/cb03cc4e5717e.webp b/exercises/04.router/01.problem.router/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/cb03cc4e5717e.webp rename to exercises/04.router/01.problem.router/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/cfd10fcd2de6c.webp b/exercises/04.router/01.problem.router/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/cfd10fcd2de6c.webp rename to exercises/04.router/01.problem.router/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/d3b8aa65ffe6c.webp b/exercises/04.router/01.problem.router/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/04.router/01.problem.router/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/d486d48b82b81.webp b/exercises/04.router/01.problem.router/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/d486d48b82b81.webp rename to exercises/04.router/01.problem.router/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/e92cefe4f6727.webp b/exercises/04.router/01.problem.router/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/e92cefe4f6727.webp rename to exercises/04.router/01.problem.router/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/ec7a3f950f99f.webp b/exercises/04.router/01.problem.router/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/ec7a3f950f99f.webp rename to exercises/04.router/01.problem.router/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/f3d9a88e1c234.webp b/exercises/04.router/01.problem.router/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/f3d9a88e1c234.webp rename to exercises/04.router/01.problem.router/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/08.problem.hydrate/public/img/ships/fdc13cb488bf1.webp b/exercises/04.router/01.problem.router/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/img/ships/fdc13cb488bf1.webp rename to exercises/04.router/01.problem.router/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/04.router/01.problem.router/public/index.html b/exercises/04.router/01.problem.router/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/04.router/01.problem.router/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/08.problem.hydrate/public/style.css b/exercises/04.router/01.problem.router/public/style.css similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/public/style.css rename to exercises/04.router/01.problem.router/public/style.css diff --git a/exercises/04.router/01.problem.router/server/app.js b/exercises/04.router/01.problem.router/server/app.js new file mode 100644 index 0000000..c311fef --- /dev/null +++ b/exercises/04.router/01.problem.router/server/app.js @@ -0,0 +1,78 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const moduleBasePath = new URL('../ui', import.meta.url).href + const { pipe } = renderToPipeableStream(h(App), moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/05.problem.bootstrap/server/async-storage.js b/exercises/04.router/01.problem.router/server/async-storage.js similarity index 100% rename from exercises/01.exercises/05.problem.bootstrap/server/async-storage.js rename to exercises/04.router/01.problem.router/server/async-storage.js diff --git a/exercises/01.exercises/10.problem.actions/server/register-rsc-loader.js b/exercises/04.router/01.problem.router/server/register-rsc-loader.js similarity index 100% rename from exercises/01.exercises/10.problem.actions/server/register-rsc-loader.js rename to exercises/04.router/01.problem.router/server/register-rsc-loader.js diff --git a/exercises/04.router/01.problem.router/server/rsc-loader.js b/exercises/04.router/01.problem.router/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/04.router/01.problem.router/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/04.router/01.problem.router/tests/client-side-routing.test.js b/exercises/04.router/01.problem.router/tests/client-side-routing.test.js new file mode 100644 index 0000000..69f3616 --- /dev/null +++ b/exercises/04.router/01.problem.router/tests/client-side-routing.test.js @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform client-side routing', async ({ + page, +}) => { + await page.goto('/') + let reloadCount = 0 + + // Listen for 'load' event which triggers on page reload + page.on('load', () => { + reloadCount++ + }) + + // Wait for the page to load + await page.waitForSelector('a') + + // Get the first link + const firstLink = await page.locator('a').first() + + // Get the href attribute of the first link + const href = await firstLink.getAttribute('href') + + // Click the first link + await firstLink.click() + + // Wait for the URL to change + await page.waitForURL(`**${href}`) + + // Verify the URL has updated + expect(page.url()).toContain(href) + + // Verify no reloads occurred + expect(reloadCount).toBe(0) +}) diff --git a/exercises/04.router/01.problem.router/tests/playwright.config.js b/exercises/04.router/01.problem.router/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/04.router/01.problem.router/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/04.router/01.problem.router/tests/smoke.test.js b/exercises/04.router/01.problem.router/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/04.router/01.problem.router/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/04.router/01.problem.router/ui/app.js b/exercises/04.router/01.problem.router/ui/app.js new file mode 100644 index 0000000..73815f4 --- /dev/null +++ b/exercises/04.router/01.problem.router/ui/app.js @@ -0,0 +1,34 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.js' +import { ShipDetails, ShipFallback, ShipError } from './ship-details.js' +import { SearchResults, SearchResultsFallback } from './ship-search-results.js' +import { ShipSearch } from './ship-search.js' + +export function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h(ShipSearch, { + search, + results: h(SearchResults, { search }), + fallback: h(SearchResultsFallback), + }), + ), + h( + 'div', + { className: 'details' }, + 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/04.router/01.problem.router/ui/edit-text.js b/exercises/04.router/01.problem.router/ui/edit-text.js new file mode 100644 index 0000000..b30c19f --- /dev/null +++ b/exercises/04.router/01.problem.router/ui/edit-text.js @@ -0,0 +1,84 @@ +'use client' + +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + null, + edit + ? h( + 'form', + { + onSubmit: (event) => { + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + ) +} diff --git a/exercises/04.router/01.problem.router/ui/error-boundary.js b/exercises/04.router/01.problem.router/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/04.router/01.problem.router/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/04.router/01.problem.router/ui/img-utils.js b/exercises/04.router/01.problem.router/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/04.router/01.problem.router/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/01.exercises/09.solution.routing/src/img.js b/exercises/04.router/01.problem.router/ui/img.js similarity index 100% rename from exercises/01.exercises/09.solution.routing/src/img.js rename to exercises/04.router/01.problem.router/ui/img.js diff --git a/exercises/04.router/01.problem.router/ui/index.js b/exercises/04.router/01.problem.router/ui/index.js new file mode 100644 index 0000000..0a8878d --- /dev/null +++ b/exercises/04.router/01.problem.router/ui/index.js @@ -0,0 +1,87 @@ +import { Suspense, createElement as h, startTransition, use } from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { + RouterContext, + getGlobalLocation, + // ๐Ÿ’ฐ you'll need this + // useLinkHandler, +} from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + }) +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +function Root() { + // ๐Ÿจ put this in state so we can update this as the user navigates + const location = initialLocation + // ๐Ÿจ put this in state so we can update this as the user navigates + const contentPromise = initialContentPromise + + // ๐Ÿจ this function should accept the nextLocation and an optional options argument + // that has a replace option which defaults to false (this will be used to + // determine whether we should call replaceState or pushState) + function navigate() { + // ๐Ÿจ set the location to the nextLocation + // ๐Ÿจ create a nextContentFetchPromise which is set to fetchContent(nextLocation) + // ๐Ÿจ add a .then handler to the fetch promise which accepts the response + // - if replace is true, call window.history.replaceState({}, '', nextLocation) + // - otherwise, call window.history.pushState({}, '', nextLocation) + // - return the response + // ๐Ÿจ create a nextContentPromise variable set to createFromFetch(nextContentFetchPromise) + // ๐Ÿจ set the content promise inside a startTransition + } + + // ๐Ÿจ call useLinkHandler with navigate so all links will navigate when clicked + + return h( + RouterContext, + { + value: { + navigate, + location, + }, + }, + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/04.router/01.problem.router/ui/router.js b/exercises/04.router/01.problem.router/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/04.router/01.problem.router/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/04.router/01.problem.router/ui/ship-details.js b/exercises/04.router/01.problem.router/ui/ship-details.js new file mode 100644 index 0000000..5fe0435 --- /dev/null +++ b/exercises/04.router/01.problem.router/ui/ship-details.js @@ -0,0 +1,113 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + initialValue: 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/04.router/01.problem.router/ui/ship-search-results.js b/exercises/04.router/01.problem.router/ui/ship-search-results.js new file mode 100644 index 0000000..67e22f3 --- /dev/null +++ b/exercises/04.router/01.problem.router/ui/ship-search-results.js @@ -0,0 +1,45 @@ +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 { SelectShipLink } 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( + SelectShipLink, + // ๐Ÿ’ฃ you can remove the search prop here now that this information is + // in our router. + { shipId: ship.id, search, 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/04.router/01.problem.router/ui/ship-search.js b/exercises/04.router/01.problem.router/ui/ship-search.js new file mode 100644 index 0000000..989dd51 --- /dev/null +++ b/exercises/04.router/01.problem.router/ui/ship-search.js @@ -0,0 +1,65 @@ +'use client' + +import { Fragment, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' + +export function ShipSearch({ search, results, fallback }) { + // ๐Ÿจ get the navigate function and location from useRouter() + return h( + Fragment, + null, + h( + 'form', + // ๐Ÿจ add a submit handler here to prevent the default full page refresh + {}, + h('input', { + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + name: 'search', + autoFocus: true, + // ๐Ÿจ add an onChange handler so we can update the search in the URL + // ๐Ÿจ use the mergeLocationState utility to create a newLocation that + // copies the state from the current location with an updated search value + // ๐Ÿจ navigate to the newLocation and set the replace option to true + }), + ), + h( + ErrorBoundary, + { fallback: ShipResultsErrorFallback }, + h('ul', null, h(Suspense, { fallback }, results)), + ), + ) +} + +// ๐Ÿ’ฃ you can remove the search prop here now that we can use the location from +// the router +export function SelectShipLink({ shipId, search, highlight, children }) { + // ๐Ÿจ get the current location from useRouter + + // ๐Ÿฆ‰ the useLinkHandler you'll add in ui/index.js will set up an event handler + // to listen to clicks to anchor elements and navigate properly. + + // right now we're merging manually, but now you can use our + // mergeLocationState utility. + // ๐Ÿจ update href to be mergeLocationState(location, { shipId }) + const href = [ + `/${shipId}`, + search ? `search=${encodeURIComponent(search)}` : null, + ] + .filter(Boolean) + .join('?') + return h('a', { + children, + href, + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/04.router/01.problem.router/ui/spin-delay.js b/exercises/04.router/01.problem.router/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/04.router/01.problem.router/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/01.exercises/08.solution.hydrate/.gitignore b/exercises/04.router/01.solution.router/.gitignore similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/.gitignore rename to exercises/04.router/01.solution.router/.gitignore diff --git a/exercises/04.router/01.solution.router/README.mdx b/exercises/04.router/01.solution.router/README.mdx new file mode 100644 index 0000000..eac3691 --- /dev/null +++ b/exercises/04.router/01.solution.router/README.mdx @@ -0,0 +1,5 @@ +# Client Router + + + +๐Ÿ‘จโ€๐Ÿ’ผ Great work! We can now navigate around without full-page reloads. diff --git a/exercises/04.router/01.solution.router/db/ship-api.js b/exercises/04.router/01.solution.router/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/04.router/01.solution.router/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/04.router/01.solution.router/db/ships.json b/exercises/04.router/01.solution.router/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/04.router/01.solution.router/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/04.router/01.solution.router/package.json b/exercises/04.router/01.solution.router/package.json new file mode 100644 index 0000000..fa936b2 --- /dev/null +++ b/exercises/04.router/01.solution.router/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__04.router__sep__01.solution.router", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/04.router/01.solution.router/public/favicon.ico b/exercises/04.router/01.solution.router/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/04.router/01.solution.router/public/favicon.ico differ diff --git a/exercises/04.router/01.solution.router/public/favicon.svg b/exercises/04.router/01.solution.router/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/04.router/01.solution.router/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/04.router/01.solution.router/public/iframe-sync.js b/exercises/04.router/01.solution.router/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/04.router/01.solution.router/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/broken-ship.webp b/exercises/04.router/01.solution.router/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/broken-ship.webp rename to exercises/04.router/01.solution.router/public/img/broken-ship.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/fallback-ship.png b/exercises/04.router/01.solution.router/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/fallback-ship.png rename to exercises/04.router/01.solution.router/public/img/fallback-ship.png diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/0268fc4817ad1.webp b/exercises/04.router/01.solution.router/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/0268fc4817ad1.webp rename to exercises/04.router/01.solution.router/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/1ae7b4b92036b.webp b/exercises/04.router/01.solution.router/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/1ae7b4b92036b.webp rename to exercises/04.router/01.solution.router/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/1ff1991efe029.webp b/exercises/04.router/01.solution.router/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/1ff1991efe029.webp rename to exercises/04.router/01.solution.router/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/3ba8aa65ffe6c.webp b/exercises/04.router/01.solution.router/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/04.router/01.solution.router/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/441f7092a8d44.webp b/exercises/04.router/01.solution.router/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/441f7092a8d44.webp rename to exercises/04.router/01.solution.router/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/5c13d8b28a14a.webp b/exercises/04.router/01.solution.router/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/5c13d8b28a14a.webp rename to exercises/04.router/01.solution.router/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/627c497212456.webp b/exercises/04.router/01.solution.router/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/627c497212456.webp rename to exercises/04.router/01.solution.router/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/670003aed3795.webp b/exercises/04.router/01.solution.router/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/670003aed3795.webp rename to exercises/04.router/01.solution.router/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/6c86fca8b9086.webp b/exercises/04.router/01.solution.router/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/6c86fca8b9086.webp rename to exercises/04.router/01.solution.router/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/6f375578ead88.webp b/exercises/04.router/01.solution.router/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/6f375578ead88.webp rename to exercises/04.router/01.solution.router/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/ab267a5984523.webp b/exercises/04.router/01.solution.router/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/ab267a5984523.webp rename to exercises/04.router/01.solution.router/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/b442531ea32b2.webp b/exercises/04.router/01.solution.router/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/b442531ea32b2.webp rename to exercises/04.router/01.solution.router/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/bc4cbadf89bd3.webp b/exercises/04.router/01.solution.router/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/bc4cbadf89bd3.webp rename to exercises/04.router/01.solution.router/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/cb03cc4e5717e.webp b/exercises/04.router/01.solution.router/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/cb03cc4e5717e.webp rename to exercises/04.router/01.solution.router/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/cfd10fcd2de6c.webp b/exercises/04.router/01.solution.router/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/cfd10fcd2de6c.webp rename to exercises/04.router/01.solution.router/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/d3b8aa65ffe6c.webp b/exercises/04.router/01.solution.router/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/04.router/01.solution.router/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/d486d48b82b81.webp b/exercises/04.router/01.solution.router/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/d486d48b82b81.webp rename to exercises/04.router/01.solution.router/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/e92cefe4f6727.webp b/exercises/04.router/01.solution.router/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/e92cefe4f6727.webp rename to exercises/04.router/01.solution.router/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/ec7a3f950f99f.webp b/exercises/04.router/01.solution.router/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/ec7a3f950f99f.webp rename to exercises/04.router/01.solution.router/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/f3d9a88e1c234.webp b/exercises/04.router/01.solution.router/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/f3d9a88e1c234.webp rename to exercises/04.router/01.solution.router/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/08.solution.hydrate/public/img/ships/fdc13cb488bf1.webp b/exercises/04.router/01.solution.router/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/img/ships/fdc13cb488bf1.webp rename to exercises/04.router/01.solution.router/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/04.router/01.solution.router/public/index.html b/exercises/04.router/01.solution.router/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/04.router/01.solution.router/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/08.solution.hydrate/public/style.css b/exercises/04.router/01.solution.router/public/style.css similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/public/style.css rename to exercises/04.router/01.solution.router/public/style.css diff --git a/exercises/04.router/01.solution.router/server/app.js b/exercises/04.router/01.solution.router/server/app.js new file mode 100644 index 0000000..c311fef --- /dev/null +++ b/exercises/04.router/01.solution.router/server/app.js @@ -0,0 +1,78 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const moduleBasePath = new URL('../ui', import.meta.url).href + const { pipe } = renderToPipeableStream(h(App), moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/05.solution.bootstrap/server/async-storage.js b/exercises/04.router/01.solution.router/server/async-storage.js similarity index 100% rename from exercises/01.exercises/05.solution.bootstrap/server/async-storage.js rename to exercises/04.router/01.solution.router/server/async-storage.js diff --git a/exercises/01.exercises/10.solution.actions/server/register-rsc-loader.js b/exercises/04.router/01.solution.router/server/register-rsc-loader.js similarity index 100% rename from exercises/01.exercises/10.solution.actions/server/register-rsc-loader.js rename to exercises/04.router/01.solution.router/server/register-rsc-loader.js diff --git a/exercises/04.router/01.solution.router/server/rsc-loader.js b/exercises/04.router/01.solution.router/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/04.router/01.solution.router/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/04.router/01.solution.router/tests/client-side-routing.test.js b/exercises/04.router/01.solution.router/tests/client-side-routing.test.js new file mode 100644 index 0000000..69f3616 --- /dev/null +++ b/exercises/04.router/01.solution.router/tests/client-side-routing.test.js @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform client-side routing', async ({ + page, +}) => { + await page.goto('/') + let reloadCount = 0 + + // Listen for 'load' event which triggers on page reload + page.on('load', () => { + reloadCount++ + }) + + // Wait for the page to load + await page.waitForSelector('a') + + // Get the first link + const firstLink = await page.locator('a').first() + + // Get the href attribute of the first link + const href = await firstLink.getAttribute('href') + + // Click the first link + await firstLink.click() + + // Wait for the URL to change + await page.waitForURL(`**${href}`) + + // Verify the URL has updated + expect(page.url()).toContain(href) + + // Verify no reloads occurred + expect(reloadCount).toBe(0) +}) diff --git a/exercises/04.router/01.solution.router/tests/playwright.config.js b/exercises/04.router/01.solution.router/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/04.router/01.solution.router/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/04.router/01.solution.router/tests/smoke.test.js b/exercises/04.router/01.solution.router/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/04.router/01.solution.router/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/04.router/01.solution.router/ui/app.js b/exercises/04.router/01.solution.router/ui/app.js new file mode 100644 index 0000000..73815f4 --- /dev/null +++ b/exercises/04.router/01.solution.router/ui/app.js @@ -0,0 +1,34 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.js' +import { ShipDetails, ShipFallback, ShipError } from './ship-details.js' +import { SearchResults, SearchResultsFallback } from './ship-search-results.js' +import { ShipSearch } from './ship-search.js' + +export function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h(ShipSearch, { + search, + results: h(SearchResults, { search }), + fallback: h(SearchResultsFallback), + }), + ), + h( + 'div', + { className: 'details' }, + 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/04.router/01.solution.router/ui/edit-text.js b/exercises/04.router/01.solution.router/ui/edit-text.js new file mode 100644 index 0000000..b30c19f --- /dev/null +++ b/exercises/04.router/01.solution.router/ui/edit-text.js @@ -0,0 +1,84 @@ +'use client' + +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + null, + edit + ? h( + 'form', + { + onSubmit: (event) => { + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + ) +} diff --git a/exercises/04.router/01.solution.router/ui/error-boundary.js b/exercises/04.router/01.solution.router/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/04.router/01.solution.router/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/04.router/01.solution.router/ui/img-utils.js b/exercises/04.router/01.solution.router/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/04.router/01.solution.router/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/01.exercises/10.problem.actions/src/img.js b/exercises/04.router/01.solution.router/ui/img.js similarity index 100% rename from exercises/01.exercises/10.problem.actions/src/img.js rename to exercises/04.router/01.solution.router/ui/img.js diff --git a/exercises/04.router/01.solution.router/ui/index.js b/exercises/04.router/01.solution.router/ui/index.js new file mode 100644 index 0000000..ad1873c --- /dev/null +++ b/exercises/04.router/01.solution.router/ui/index.js @@ -0,0 +1,90 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useState, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + }) +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +function Root() { + const [location, setLocation] = useState(initialLocation) + const [contentPromise, setContentPromise] = useState(initialContentPromise) + + function navigate(nextLocation, { replace = false } = {}) { + setLocation(nextLocation) + + const nextContentFetchPromise = fetchContent(nextLocation).then( + (response) => { + if (replace) { + window.history.replaceState({}, '', nextLocation) + } else { + window.history.pushState({}, '', nextLocation) + } + return response + }, + ) + const nextContentPromise = createFromFetch(nextContentFetchPromise) + + startTransition(() => setContentPromise(nextContentPromise)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + }, + }, + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/04.router/01.solution.router/ui/router.js b/exercises/04.router/01.solution.router/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/04.router/01.solution.router/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/04.router/01.solution.router/ui/ship-details.js b/exercises/04.router/01.solution.router/ui/ship-details.js new file mode 100644 index 0000000..5fe0435 --- /dev/null +++ b/exercises/04.router/01.solution.router/ui/ship-details.js @@ -0,0 +1,113 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + initialValue: 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/04.router/01.solution.router/ui/ship-search-results.js b/exercises/04.router/01.solution.router/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/04.router/01.solution.router/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/04.router/01.solution.router/ui/ship-search.js b/exercises/04.router/01.solution.router/ui/ship-search.js new file mode 100644 index 0000000..6314601 --- /dev/null +++ b/exercises/04.router/01.solution.router/ui/ship-search.js @@ -0,0 +1,53 @@ +'use client' + +import { Fragment, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { mergeLocationState, useRouter } from './router.js' + +export function ShipSearch({ search, results, fallback }) { + const { navigate, location } = useRouter() + + 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: ShipResultsErrorFallback }, + h('ul', null, h(Suspense, { fallback }, results)), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/04.router/01.solution.router/ui/spin-delay.js b/exercises/04.router/01.solution.router/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/04.router/01.solution.router/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/01.exercises/09.problem.routing/.gitignore b/exercises/04.router/02.problem.pending-ui/.gitignore similarity index 100% rename from exercises/01.exercises/09.problem.routing/.gitignore rename to exercises/04.router/02.problem.pending-ui/.gitignore diff --git a/exercises/04.router/02.problem.pending-ui/README.mdx b/exercises/04.router/02.problem.pending-ui/README.mdx new file mode 100644 index 0000000..e312afc --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/README.mdx @@ -0,0 +1,38 @@ +# Pending UI + + + +๐Ÿ‘จโ€๐Ÿ’ผ Sometimes our users are on slow networks so we should give them good feedback +when they interact with our application. + +To be able to do this effectively, we need a few things: + +1. Our transitions should take place within a `useTransition` `startTransition` +2. The `location` to remain unchanged until the transition is complete +3. We need a `nextLocation` state so we can determine what part of the location + is changing + +We've already got our transition wrapped in a `startTransition`, but this is the +global one from `react`. We need to use one from `useTransition` instead so we +get access to the `isPending` state. + +Then we'll change our `location` to be a `nextLocation` and then use +`useDeferredValue` to get the `location` so that it remains unchanged until the +transition is complete. + +Then we can add the `isPending` and `nextLocation` to our router context and use +that to determine pending states for our UI. + +๐Ÿงโ€โ™‚๏ธ You're going to want to use the `parseLocationState` utility I made for this +one. Here's how it works: + +```js +import { parseLocationState } from './router.js' + +const location = '/abc123?search=starship' +const state = parseLocationState(location) +// ^ { shipId: 'abc123', search: 'starship' } +``` + +You can use that to parse the current location and the next location. If the +part you care about is different then you know you can show a pending state. diff --git a/exercises/04.router/02.problem.pending-ui/db/ship-api.js b/exercises/04.router/02.problem.pending-ui/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/04.router/02.problem.pending-ui/db/ships.json b/exercises/04.router/02.problem.pending-ui/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/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/04.router/02.problem.pending-ui/package.json b/exercises/04.router/02.problem.pending-ui/package.json new file mode 100644 index 0000000..9c3796c --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__04.router__sep__02.problem.pending-ui", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/04.router/02.problem.pending-ui/public/favicon.ico b/exercises/04.router/02.problem.pending-ui/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/04.router/02.problem.pending-ui/public/favicon.ico differ diff --git a/exercises/04.router/02.problem.pending-ui/public/favicon.svg b/exercises/04.router/02.problem.pending-ui/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/04.router/02.problem.pending-ui/public/iframe-sync.js b/exercises/04.router/02.problem.pending-ui/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/09.problem.routing/public/img/broken-ship.webp b/exercises/04.router/02.problem.pending-ui/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/broken-ship.webp rename to exercises/04.router/02.problem.pending-ui/public/img/broken-ship.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/fallback-ship.png b/exercises/04.router/02.problem.pending-ui/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/fallback-ship.png rename to exercises/04.router/02.problem.pending-ui/public/img/fallback-ship.png diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/0268fc4817ad1.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/0268fc4817ad1.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/1ae7b4b92036b.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/1ae7b4b92036b.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/1ff1991efe029.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/1ff1991efe029.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/3ba8aa65ffe6c.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/441f7092a8d44.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/441f7092a8d44.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/5c13d8b28a14a.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/5c13d8b28a14a.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/627c497212456.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/627c497212456.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/670003aed3795.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/670003aed3795.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/6c86fca8b9086.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/6c86fca8b9086.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/6f375578ead88.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/6f375578ead88.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/ab267a5984523.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/ab267a5984523.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/b442531ea32b2.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/b442531ea32b2.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/bc4cbadf89bd3.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/bc4cbadf89bd3.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/cb03cc4e5717e.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/cb03cc4e5717e.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/cfd10fcd2de6c.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/cfd10fcd2de6c.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/d3b8aa65ffe6c.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/d486d48b82b81.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/d486d48b82b81.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/e92cefe4f6727.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/e92cefe4f6727.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/ec7a3f950f99f.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/ec7a3f950f99f.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/f3d9a88e1c234.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/f3d9a88e1c234.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/09.problem.routing/public/img/ships/fdc13cb488bf1.webp b/exercises/04.router/02.problem.pending-ui/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/img/ships/fdc13cb488bf1.webp rename to exercises/04.router/02.problem.pending-ui/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/04.router/02.problem.pending-ui/public/index.html b/exercises/04.router/02.problem.pending-ui/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/09.problem.routing/public/style.css b/exercises/04.router/02.problem.pending-ui/public/style.css similarity index 100% rename from exercises/01.exercises/09.problem.routing/public/style.css rename to exercises/04.router/02.problem.pending-ui/public/style.css diff --git a/exercises/04.router/02.problem.pending-ui/server/app.js b/exercises/04.router/02.problem.pending-ui/server/app.js new file mode 100644 index 0000000..c311fef --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/server/app.js @@ -0,0 +1,78 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const moduleBasePath = new URL('../ui', import.meta.url).href + const { pipe } = renderToPipeableStream(h(App), moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/06.problem.import-map/server/async-storage.js b/exercises/04.router/02.problem.pending-ui/server/async-storage.js similarity index 100% rename from exercises/01.exercises/06.problem.import-map/server/async-storage.js rename to exercises/04.router/02.problem.pending-ui/server/async-storage.js diff --git a/exercises/04.router/02.problem.pending-ui/server/register-rsc-loader.js b/exercises/04.router/02.problem.pending-ui/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/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/04.router/02.problem.pending-ui/server/rsc-loader.js b/exercises/04.router/02.problem.pending-ui/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/04.router/02.problem.pending-ui/tests/pending-ui.test.js b/exercises/04.router/02.problem.pending-ui/tests/pending-ui.test.js new file mode 100644 index 0000000..363ff62 --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/tests/pending-ui.test.js @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test' +import { searchShips } from '../db/ship-api.js' + +test('should display pending UI when performing a search and selecting a ship', async ({ + page, +}) => { + const { + ships: [firstShip], + } = await searchShips({ search: '' }) + await page.goto(`/${firstShip.id}`) + + await page.waitForSelector('li a:has-text("... loading")', { + state: 'detached', + }) + + // simulate a slow network for the /rsc endpoint so we force the pending UI to show up + await page.route('/rsc/*', async (route) => { + await new Promise((resolve) => + setTimeout(resolve, process.env.CI ? 1000 : 400), + ) + await route.continue() + }) + + const searchInput = page.getByRole('searchbox', { name: /ships/i }) + await searchInput.fill('s') + + await expect(page.getByRole('list').first()).toHaveCSS('opacity', '0.6') + + await expect(page.getByRole('list').first()).toHaveCSS('opacity', '1') + + const firstShipLink = page.locator('li a').first() + const shipName = await firstShipLink.textContent() + await firstShipLink.click() + + await expect(page.locator('.details')).toHaveCSS('opacity', '0.6') + + await expect(page.locator('.details')).toHaveCSS('opacity', '1') + + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/04.router/02.problem.pending-ui/tests/playwright.config.js b/exercises/04.router/02.problem.pending-ui/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/04.router/02.problem.pending-ui/tests/smoke.test.js b/exercises/04.router/02.problem.pending-ui/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/04.router/02.problem.pending-ui/ui/app.js b/exercises/04.router/02.problem.pending-ui/ui/app.js new file mode 100644 index 0000000..fb8b4b9 --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/ui/app.js @@ -0,0 +1,43 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.js' +// ๐Ÿจ you're going to want this +// 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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + h( + 'div', + { className: 'search' }, + h(ShipSearch, { + search, + results: h(SearchResults, { search }), + fallback: h(SearchResultsFallback), + }), + ), + h( + // ๐Ÿจ we can't use useRouter in here because this is a server component + // and can't use state. So we moved this div with the details className + // into ./ship-details-pending.js which is a client component. Replace + // this div with the ShipDetailsPendingTransition component + 'div', + // ๐Ÿ’ฐ ShipDetailsPendingTransition doesn't need any props, so you can pass null here. + { className: 'details' }, + // ๐Ÿฆ‰ the rest of this can be unchanged. Cool how we can pass server + // components to the client components, right?! + 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/04.router/02.problem.pending-ui/ui/edit-text.js b/exercises/04.router/02.problem.pending-ui/ui/edit-text.js new file mode 100644 index 0000000..b30c19f --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/ui/edit-text.js @@ -0,0 +1,84 @@ +'use client' + +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + null, + edit + ? h( + 'form', + { + onSubmit: (event) => { + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + ) +} diff --git a/exercises/04.router/02.problem.pending-ui/ui/error-boundary.js b/exercises/04.router/02.problem.pending-ui/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/04.router/02.problem.pending-ui/ui/img-utils.js b/exercises/04.router/02.problem.pending-ui/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/01.exercises/10.solution.actions/src/img.js b/exercises/04.router/02.problem.pending-ui/ui/img.js similarity index 100% rename from exercises/01.exercises/10.solution.actions/src/img.js rename to exercises/04.router/02.problem.pending-ui/ui/img.js diff --git a/exercises/04.router/02.problem.pending-ui/ui/index.js b/exercises/04.router/02.problem.pending-ui/ui/index.js new file mode 100644 index 0000000..07b0083 --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/ui/index.js @@ -0,0 +1,98 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + // ๐Ÿ’ฐ you'll need this + // useDeferredValue, + useState, + // ๐Ÿ’ฐ you'll need this + // useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + }) +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +function Root() { + // ๐Ÿจ change this to nextLocation + const [location, setLocation] = useState(initialLocation) + const [contentPromise, setContentPromise] = useState(initialContentPromise) + // ๐Ÿจ call useTransition here to get isPending and startTransition + + // ๐Ÿจ create a location variable set to useDeferredValue of the nextLocation + + function navigate(nextLocation, { replace = false } = {}) { + setLocation(nextLocation) + + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (replace) { + window.history.replaceState({}, '', nextLocation) + } else { + window.history.pushState({}, '', nextLocation) + } + return response + }), + ) + + startTransition(() => setContentPromise(nextContentPromise)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + // ๐Ÿจ add the nextLocation and isPending to this context value + }, + }, + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/04.router/02.problem.pending-ui/ui/router.js b/exercises/04.router/02.problem.pending-ui/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/04.router/02.problem.pending-ui/ui/ship-details-pending.js b/exercises/04.router/02.problem.pending-ui/ui/ship-details-pending.js new file mode 100644 index 0000000..f48ad02 --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/ui/ship-details-pending.js @@ -0,0 +1,22 @@ +'use client' + +import { createElement as h } from 'react' +// ๐Ÿ’ฐ you'll want this +// import { parseLocationState, useRouter } from './router.js' +// ๐Ÿ’ฏ if you want to do the extra credit, grab this +// import { useSpinDelay } from './spin-delay.js' + +export function ShipDetailsPendingTransition({ children }) { + // ๐Ÿจ get the location and nextLocation from useRouter + // ๐Ÿจ the details are pending if the shipId of the nextLocation differs from + // the shipId of the current location + // ๐Ÿ’ฐ use parseLocationState to get the shipId. + // ๐Ÿ’ฏ for extra credit, avoid a flash of loading state with useSpinDelay + const isShipDetailsPending = false + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/04.router/02.problem.pending-ui/ui/ship-details.js b/exercises/04.router/02.problem.pending-ui/ui/ship-details.js new file mode 100644 index 0000000..5fe0435 --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/ui/ship-details.js @@ -0,0 +1,113 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + initialValue: 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/04.router/02.problem.pending-ui/ui/ship-search-results.js b/exercises/04.router/02.problem.pending-ui/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/04.router/02.problem.pending-ui/ui/ship-search.js b/exercises/04.router/02.problem.pending-ui/ui/ship-search.js new file mode 100644 index 0000000..72d53cb --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/ui/ship-search.js @@ -0,0 +1,66 @@ +'use client' + +import { Fragment, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +// ๐Ÿ’ฐ bring in parseLocationState here +import { mergeLocationState, useRouter } from './router.js' +// ๐Ÿ’ฏ if you want to do the extra credit, you'll want this: +// import { useSpinDelay } from './spin-delay.js' + +export function ShipSearch({ search, results, fallback }) { + // ๐Ÿจ get the nextLocation here + const { navigate, location } = useRouter() + // ๐Ÿจ we're pending if the nextLocation's search is different from the current + // location's search + // ๐Ÿ’ฐ you'll want to use parseLocationState for this + // ๐Ÿ’ฏ for extra credit, avoid a flash of loading state with useSpinDelay + const isShipSearchPending = false + + 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/04.router/02.problem.pending-ui/ui/spin-delay.js b/exercises/04.router/02.problem.pending-ui/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/04.router/02.problem.pending-ui/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/01.exercises/09.solution.routing/.gitignore b/exercises/04.router/02.solution.pending-ui/.gitignore similarity index 100% rename from exercises/01.exercises/09.solution.routing/.gitignore rename to exercises/04.router/02.solution.pending-ui/.gitignore diff --git a/exercises/04.router/02.solution.pending-ui/README.mdx b/exercises/04.router/02.solution.pending-ui/README.mdx new file mode 100644 index 0000000..b212228 --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/README.mdx @@ -0,0 +1,7 @@ +# Pending UI + + + +๐Ÿ‘จโ€๐Ÿ’ผ Great! The URL is the source of truth. We can provide our components useful +information about the transitions going on so they can display pending UI as +needed. diff --git a/exercises/04.router/02.solution.pending-ui/db/ship-api.js b/exercises/04.router/02.solution.pending-ui/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/04.router/02.solution.pending-ui/db/ships.json b/exercises/04.router/02.solution.pending-ui/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/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/04.router/02.solution.pending-ui/package.json b/exercises/04.router/02.solution.pending-ui/package.json new file mode 100644 index 0000000..86796c1 --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__04.router__sep__02.solution.pending-ui", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/04.router/02.solution.pending-ui/public/favicon.ico b/exercises/04.router/02.solution.pending-ui/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/04.router/02.solution.pending-ui/public/favicon.ico differ diff --git a/exercises/04.router/02.solution.pending-ui/public/favicon.svg b/exercises/04.router/02.solution.pending-ui/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/04.router/02.solution.pending-ui/public/iframe-sync.js b/exercises/04.router/02.solution.pending-ui/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/09.solution.routing/public/img/broken-ship.webp b/exercises/04.router/02.solution.pending-ui/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/broken-ship.webp rename to exercises/04.router/02.solution.pending-ui/public/img/broken-ship.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/fallback-ship.png b/exercises/04.router/02.solution.pending-ui/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/fallback-ship.png rename to exercises/04.router/02.solution.pending-ui/public/img/fallback-ship.png diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/0268fc4817ad1.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/0268fc4817ad1.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/1ae7b4b92036b.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/1ae7b4b92036b.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/1ff1991efe029.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/1ff1991efe029.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/3ba8aa65ffe6c.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/441f7092a8d44.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/441f7092a8d44.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/5c13d8b28a14a.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/5c13d8b28a14a.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/627c497212456.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/627c497212456.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/670003aed3795.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/670003aed3795.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/6c86fca8b9086.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/6c86fca8b9086.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/6f375578ead88.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/6f375578ead88.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/ab267a5984523.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/ab267a5984523.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/b442531ea32b2.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/b442531ea32b2.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/bc4cbadf89bd3.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/bc4cbadf89bd3.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/cb03cc4e5717e.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/cb03cc4e5717e.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/cfd10fcd2de6c.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/cfd10fcd2de6c.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/d3b8aa65ffe6c.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/d486d48b82b81.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/d486d48b82b81.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/e92cefe4f6727.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/e92cefe4f6727.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/ec7a3f950f99f.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/ec7a3f950f99f.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/f3d9a88e1c234.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/f3d9a88e1c234.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/09.solution.routing/public/img/ships/fdc13cb488bf1.webp b/exercises/04.router/02.solution.pending-ui/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/img/ships/fdc13cb488bf1.webp rename to exercises/04.router/02.solution.pending-ui/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/04.router/02.solution.pending-ui/public/index.html b/exercises/04.router/02.solution.pending-ui/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/09.solution.routing/public/style.css b/exercises/04.router/02.solution.pending-ui/public/style.css similarity index 100% rename from exercises/01.exercises/09.solution.routing/public/style.css rename to exercises/04.router/02.solution.pending-ui/public/style.css diff --git a/exercises/04.router/02.solution.pending-ui/server/app.js b/exercises/04.router/02.solution.pending-ui/server/app.js new file mode 100644 index 0000000..c311fef --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/server/app.js @@ -0,0 +1,78 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const moduleBasePath = new URL('../ui', import.meta.url).href + const { pipe } = renderToPipeableStream(h(App), moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/06.solution.import-map/server/async-storage.js b/exercises/04.router/02.solution.pending-ui/server/async-storage.js similarity index 100% rename from exercises/01.exercises/06.solution.import-map/server/async-storage.js rename to exercises/04.router/02.solution.pending-ui/server/async-storage.js diff --git a/exercises/04.router/02.solution.pending-ui/server/register-rsc-loader.js b/exercises/04.router/02.solution.pending-ui/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/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/04.router/02.solution.pending-ui/server/rsc-loader.js b/exercises/04.router/02.solution.pending-ui/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/04.router/02.solution.pending-ui/tests/pending-ui.test.js b/exercises/04.router/02.solution.pending-ui/tests/pending-ui.test.js new file mode 100644 index 0000000..363ff62 --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/tests/pending-ui.test.js @@ -0,0 +1,41 @@ +import { test, expect } from '@playwright/test' +import { searchShips } from '../db/ship-api.js' + +test('should display pending UI when performing a search and selecting a ship', async ({ + page, +}) => { + const { + ships: [firstShip], + } = await searchShips({ search: '' }) + await page.goto(`/${firstShip.id}`) + + await page.waitForSelector('li a:has-text("... loading")', { + state: 'detached', + }) + + // simulate a slow network for the /rsc endpoint so we force the pending UI to show up + await page.route('/rsc/*', async (route) => { + await new Promise((resolve) => + setTimeout(resolve, process.env.CI ? 1000 : 400), + ) + await route.continue() + }) + + const searchInput = page.getByRole('searchbox', { name: /ships/i }) + await searchInput.fill('s') + + await expect(page.getByRole('list').first()).toHaveCSS('opacity', '0.6') + + await expect(page.getByRole('list').first()).toHaveCSS('opacity', '1') + + const firstShipLink = page.locator('li a').first() + const shipName = await firstShipLink.textContent() + await firstShipLink.click() + + await expect(page.locator('.details')).toHaveCSS('opacity', '0.6') + + await expect(page.locator('.details')).toHaveCSS('opacity', '1') + + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/04.router/02.solution.pending-ui/tests/playwright.config.js b/exercises/04.router/02.solution.pending-ui/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/04.router/02.solution.pending-ui/tests/smoke.test.js b/exercises/04.router/02.solution.pending-ui/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/04.router/02.solution.pending-ui/ui/app.js b/exercises/04.router/02.solution.pending-ui/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/04.router/02.solution.pending-ui/ui/edit-text.js b/exercises/04.router/02.solution.pending-ui/ui/edit-text.js new file mode 100644 index 0000000..b30c19f --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/ui/edit-text.js @@ -0,0 +1,84 @@ +'use client' + +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + null, + edit + ? h( + 'form', + { + onSubmit: (event) => { + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + ) +} diff --git a/exercises/04.router/02.solution.pending-ui/ui/error-boundary.js b/exercises/04.router/02.solution.pending-ui/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/04.router/02.solution.pending-ui/ui/img-utils.js b/exercises/04.router/02.solution.pending-ui/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/04.router/02.solution.pending-ui/ui/img.js b/exercises/04.router/02.solution.pending-ui/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/04.router/02.solution.pending-ui/ui/index.js b/exercises/04.router/02.solution.pending-ui/ui/index.js new file mode 100644 index 0000000..c554c1b --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/ui/index.js @@ -0,0 +1,96 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + }) +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +function Root() { + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentPromise, setContentPromise] = useState(initialContentPromise) + const [isPending, startTransition] = useTransition() + + const location = useDeferredValue(nextLocation) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (replace) { + window.history.replaceState({}, '', nextLocation) + } else { + window.history.pushState({}, '', nextLocation) + } + return response + }), + ) + + startTransition(() => setContentPromise(nextContentPromise)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/04.router/02.solution.pending-ui/ui/router.js b/exercises/04.router/02.solution.pending-ui/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/04.router/02.solution.pending-ui/ui/ship-details-pending.js b/exercises/04.router/02.solution.pending-ui/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/04.router/02.solution.pending-ui/ui/ship-details.js b/exercises/04.router/02.solution.pending-ui/ui/ship-details.js new file mode 100644 index 0000000..5fe0435 --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/ui/ship-details.js @@ -0,0 +1,113 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + initialValue: 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/04.router/02.solution.pending-ui/ui/ship-search-results.js b/exercises/04.router/02.solution.pending-ui/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/04.router/02.solution.pending-ui/ui/ship-search.js b/exercises/04.router/02.solution.pending-ui/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/04.router/02.solution.pending-ui/ui/spin-delay.js b/exercises/04.router/02.solution.pending-ui/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/04.router/02.solution.pending-ui/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/01.exercises/10.problem.actions/.gitignore b/exercises/04.router/03.problem.race-conditions/.gitignore similarity index 100% rename from exercises/01.exercises/10.problem.actions/.gitignore rename to exercises/04.router/03.problem.race-conditions/.gitignore diff --git a/exercises/04.router/03.problem.race-conditions/README.mdx b/exercises/04.router/03.problem.race-conditions/README.mdx new file mode 100644 index 0000000..3f54eab --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/README.mdx @@ -0,0 +1,51 @@ +# Race Conditions + + + +๐Ÿ‘จโ€๐Ÿ’ผ Networks are unpredictable. Let's imagine the user clicks around a lot and +that triggers several network requests: + +``` +*click* ------ request --------> update ui + *click* ------ request --------> update ui + *click* ------ request --------> update ui +``` + +That works nicely. But sometimes the requests come back in a different order for +whatever reason (network latency, server load, etc): + +``` +*click* --------- request ----------> update ui + *click* ---- request ------> update ui + *click* ----- request -------> update ui +``` + +This would be a pretty bad user experience because the UI would be updating in +a different order than the user clicked. + +What would be better is if we avoid updating the UI if there's a newer request +going out. + +This is easier than you might think: + +```jsx +const latestNav = useRef(null) + +// when we start a navigation: +const thisNav = Symbol() +latestNav.current = thisNav + +// when we're ready to update the UI: +if (latestNav.current === thisNav) { + // update the UI +} +``` + +That way any navigation that comes back out of order will be ignored. + +๐Ÿงโ€โ™‚๏ธ I've added a bit of code in to allow you +to simulate a race condition. Do a search for "star" and you'll notice after two +seconds, the URL gets updated to "?search=st" because we have a delay for that +search term. When you're finished, this should not happen. + +Go ahead and make that magic happen! diff --git a/exercises/04.router/03.problem.race-conditions/db/ship-api.js b/exercises/04.router/03.problem.race-conditions/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/04.router/03.problem.race-conditions/db/ships.json b/exercises/04.router/03.problem.race-conditions/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/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/04.router/03.problem.race-conditions/package.json b/exercises/04.router/03.problem.race-conditions/package.json new file mode 100644 index 0000000..74f8d6a --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__04.router__sep__03.problem.race-conditions", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/04.router/03.problem.race-conditions/public/favicon.ico b/exercises/04.router/03.problem.race-conditions/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/04.router/03.problem.race-conditions/public/favicon.ico differ diff --git a/exercises/04.router/03.problem.race-conditions/public/favicon.svg b/exercises/04.router/03.problem.race-conditions/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/04.router/03.problem.race-conditions/public/iframe-sync.js b/exercises/04.router/03.problem.race-conditions/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/10.problem.actions/public/img/broken-ship.webp b/exercises/04.router/03.problem.race-conditions/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/broken-ship.webp rename to exercises/04.router/03.problem.race-conditions/public/img/broken-ship.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/fallback-ship.png b/exercises/04.router/03.problem.race-conditions/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/fallback-ship.png rename to exercises/04.router/03.problem.race-conditions/public/img/fallback-ship.png diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/0268fc4817ad1.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/0268fc4817ad1.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/1ae7b4b92036b.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/1ae7b4b92036b.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/1ff1991efe029.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/1ff1991efe029.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/3ba8aa65ffe6c.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/441f7092a8d44.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/441f7092a8d44.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/5c13d8b28a14a.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/5c13d8b28a14a.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/627c497212456.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/627c497212456.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/670003aed3795.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/670003aed3795.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/6c86fca8b9086.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/6c86fca8b9086.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/6f375578ead88.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/6f375578ead88.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/ab267a5984523.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/ab267a5984523.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/b442531ea32b2.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/b442531ea32b2.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/bc4cbadf89bd3.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/bc4cbadf89bd3.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/cb03cc4e5717e.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/cb03cc4e5717e.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/cfd10fcd2de6c.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/cfd10fcd2de6c.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/d3b8aa65ffe6c.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/d486d48b82b81.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/d486d48b82b81.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/e92cefe4f6727.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/e92cefe4f6727.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/ec7a3f950f99f.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/ec7a3f950f99f.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/f3d9a88e1c234.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/f3d9a88e1c234.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/10.problem.actions/public/img/ships/fdc13cb488bf1.webp b/exercises/04.router/03.problem.race-conditions/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/img/ships/fdc13cb488bf1.webp rename to exercises/04.router/03.problem.race-conditions/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/04.router/03.problem.race-conditions/public/index.html b/exercises/04.router/03.problem.race-conditions/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/10.problem.actions/public/style.css b/exercises/04.router/03.problem.race-conditions/public/style.css similarity index 100% rename from exercises/01.exercises/10.problem.actions/public/style.css rename to exercises/04.router/03.problem.race-conditions/public/style.css diff --git a/exercises/04.router/03.problem.race-conditions/server/app.js b/exercises/04.router/03.problem.race-conditions/server/app.js new file mode 100644 index 0000000..1fc6f00 --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/server/app.js @@ -0,0 +1,86 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + // ๐Ÿงโ€โ™‚๏ธ I've added this so you can simulate a race condition + // type the search query "star" and you'll notice the URL gets + // updated to have a search of "st" after two seconds because + // this responds after later requests. When you're finished, this + // should not happen. + if (context.req.query('search') === 'st') { + await new Promise((resolve) => setTimeout(resolve, 2000)) + } + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const moduleBasePath = new URL('../ui', import.meta.url).href + const { pipe } = renderToPipeableStream(h(App), moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/07.problem.module-graph/server/async-storage.js b/exercises/04.router/03.problem.race-conditions/server/async-storage.js similarity index 100% rename from exercises/01.exercises/07.problem.module-graph/server/async-storage.js rename to exercises/04.router/03.problem.race-conditions/server/async-storage.js diff --git a/exercises/04.router/03.problem.race-conditions/server/register-rsc-loader.js b/exercises/04.router/03.problem.race-conditions/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/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/04.router/03.problem.race-conditions/server/rsc-loader.js b/exercises/04.router/03.problem.race-conditions/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/04.router/03.problem.race-conditions/tests/playwright.config.js b/exercises/04.router/03.problem.race-conditions/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/04.router/03.problem.race-conditions/tests/race-conditions.test.js b/exercises/04.router/03.problem.race-conditions/tests/race-conditions.test.js new file mode 100644 index 0000000..079fbbb --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/tests/race-conditions.test.js @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test' + +test('should not update URL for out-of-order responses', async ({ page }) => { + await page.goto('/') + + // Simulate varying network delays + await page.route('/rsc/*', async (route) => { + const url = route.request().url() + if (url.includes('search=st')) { + await new Promise((resolve) => setTimeout(resolve, 2000)) // Longer delay for 'st' + } + await route.continue() + }) + + const searchInput = page.getByRole('searchbox', { name: /ships/i }) + + // Perform rapid searches + await searchInput.fill('s') + await searchInput.fill('st') + await searchInput.fill('sta') + + // Wait for the last search to complete + await page.waitForURL('**/*?search=sta') + + // Wait for all in-flight requests to finish + await page.waitForLoadState('networkidle') + + // Check that the URL ends with 'sta', not 'st' + expect(page.url()).toMatch(/search=sta$/) +}) diff --git a/exercises/04.router/03.problem.race-conditions/tests/smoke.test.js b/exercises/04.router/03.problem.race-conditions/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/04.router/03.problem.race-conditions/ui/app.js b/exercises/04.router/03.problem.race-conditions/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/04.router/03.problem.race-conditions/ui/edit-text.js b/exercises/04.router/03.problem.race-conditions/ui/edit-text.js new file mode 100644 index 0000000..b30c19f --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/ui/edit-text.js @@ -0,0 +1,84 @@ +'use client' + +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + null, + edit + ? h( + 'form', + { + onSubmit: (event) => { + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + ) +} diff --git a/exercises/04.router/03.problem.race-conditions/ui/error-boundary.js b/exercises/04.router/03.problem.race-conditions/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/04.router/03.problem.race-conditions/ui/img-utils.js b/exercises/04.router/03.problem.race-conditions/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/04.router/03.problem.race-conditions/ui/img.js b/exercises/04.router/03.problem.race-conditions/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/04.router/03.problem.race-conditions/ui/index.js b/exercises/04.router/03.problem.race-conditions/ui/index.js new file mode 100644 index 0000000..e8cd5e0 --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/ui/index.js @@ -0,0 +1,102 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + // ๐Ÿ’ฐ you'll need this + // useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + }) +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +function Root() { + // ๐Ÿจ create a latestNav ref here which you can initialize to null if you like + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentPromise, setContentPromise] = useState(initialContentPromise) + const [isPending, startTransition] = useTransition() + + const location = useDeferredValue(nextLocation) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + // ๐Ÿจ create a Symbol for this nav (๐Ÿ’ฏ give it a descriptive label for debugging) + // ๐Ÿจ set the latestNav.current to this nav + + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + // ๐Ÿจ if the latestNav.current is no longer set to this nav, return early + if (replace) { + window.history.replaceState({}, '', nextLocation) + } else { + window.history.pushState({}, '', nextLocation) + } + return response + }), + ) + + startTransition(() => setContentPromise(nextContentPromise)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/04.router/03.problem.race-conditions/ui/router.js b/exercises/04.router/03.problem.race-conditions/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/04.router/03.problem.race-conditions/ui/ship-details-pending.js b/exercises/04.router/03.problem.race-conditions/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/04.router/03.problem.race-conditions/ui/ship-details.js b/exercises/04.router/03.problem.race-conditions/ui/ship-details.js new file mode 100644 index 0000000..5fe0435 --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/ui/ship-details.js @@ -0,0 +1,113 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + initialValue: 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/04.router/03.problem.race-conditions/ui/ship-search-results.js b/exercises/04.router/03.problem.race-conditions/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/04.router/03.problem.race-conditions/ui/ship-search.js b/exercises/04.router/03.problem.race-conditions/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/04.router/03.problem.race-conditions/ui/spin-delay.js b/exercises/04.router/03.problem.race-conditions/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/04.router/03.problem.race-conditions/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/01.exercises/10.solution.actions/.gitignore b/exercises/04.router/03.solution.race-conditions/.gitignore similarity index 100% rename from exercises/01.exercises/10.solution.actions/.gitignore rename to exercises/04.router/03.solution.race-conditions/.gitignore diff --git a/exercises/04.router/03.solution.race-conditions/README.mdx b/exercises/04.router/03.solution.race-conditions/README.mdx new file mode 100644 index 0000000..26e28c6 --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/README.mdx @@ -0,0 +1,6 @@ +# Race Conditions + + + +๐Ÿ‘จโ€๐Ÿ’ผ Great! This is a simple thing to solve, but definitely an important one in +every router. And you did a great job! diff --git a/exercises/04.router/03.solution.race-conditions/db/ship-api.js b/exercises/04.router/03.solution.race-conditions/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/04.router/03.solution.race-conditions/db/ships.json b/exercises/04.router/03.solution.race-conditions/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/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/04.router/03.solution.race-conditions/package.json b/exercises/04.router/03.solution.race-conditions/package.json new file mode 100644 index 0000000..606c97f --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__04.router__sep__03.solution.race-conditions", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/04.router/03.solution.race-conditions/public/favicon.ico b/exercises/04.router/03.solution.race-conditions/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/04.router/03.solution.race-conditions/public/favicon.ico differ diff --git a/exercises/04.router/03.solution.race-conditions/public/favicon.svg b/exercises/04.router/03.solution.race-conditions/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/04.router/03.solution.race-conditions/public/iframe-sync.js b/exercises/04.router/03.solution.race-conditions/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/01.exercises/10.solution.actions/public/img/broken-ship.webp b/exercises/04.router/03.solution.race-conditions/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/broken-ship.webp rename to exercises/04.router/03.solution.race-conditions/public/img/broken-ship.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/fallback-ship.png b/exercises/04.router/03.solution.race-conditions/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/fallback-ship.png rename to exercises/04.router/03.solution.race-conditions/public/img/fallback-ship.png diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/0268fc4817ad1.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/0268fc4817ad1.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/1ae7b4b92036b.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/1ae7b4b92036b.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/1ff1991efe029.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/1ff1991efe029.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/3ba8aa65ffe6c.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/441f7092a8d44.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/441f7092a8d44.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/5c13d8b28a14a.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/5c13d8b28a14a.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/627c497212456.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/627c497212456.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/670003aed3795.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/670003aed3795.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/6c86fca8b9086.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/6c86fca8b9086.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/6f375578ead88.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/6f375578ead88.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/ab267a5984523.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/ab267a5984523.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/b442531ea32b2.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/b442531ea32b2.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/bc4cbadf89bd3.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/bc4cbadf89bd3.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/cb03cc4e5717e.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/cb03cc4e5717e.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/cfd10fcd2de6c.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/cfd10fcd2de6c.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/d3b8aa65ffe6c.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/d486d48b82b81.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/d486d48b82b81.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/e92cefe4f6727.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/e92cefe4f6727.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/ec7a3f950f99f.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/ec7a3f950f99f.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/f3d9a88e1c234.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/f3d9a88e1c234.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/10.solution.actions/public/img/ships/fdc13cb488bf1.webp b/exercises/04.router/03.solution.race-conditions/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/img/ships/fdc13cb488bf1.webp rename to exercises/04.router/03.solution.race-conditions/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/04.router/03.solution.race-conditions/public/index.html b/exercises/04.router/03.solution.race-conditions/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/01.exercises/10.solution.actions/public/style.css b/exercises/04.router/03.solution.race-conditions/public/style.css similarity index 100% rename from exercises/01.exercises/10.solution.actions/public/style.css rename to exercises/04.router/03.solution.race-conditions/public/style.css diff --git a/exercises/04.router/03.solution.race-conditions/server/app.js b/exercises/04.router/03.solution.race-conditions/server/app.js new file mode 100644 index 0000000..1fc6f00 --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/server/app.js @@ -0,0 +1,86 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + // ๐Ÿงโ€โ™‚๏ธ I've added this so you can simulate a race condition + // type the search query "star" and you'll notice the URL gets + // updated to have a search of "st" after two seconds because + // this responds after later requests. When you're finished, this + // should not happen. + if (context.req.query('search') === 'st') { + await new Promise((resolve) => setTimeout(resolve, 2000)) + } + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const moduleBasePath = new URL('../ui', import.meta.url).href + const { pipe } = renderToPipeableStream(h(App), moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/07.solution.module-graph/server/async-storage.js b/exercises/04.router/03.solution.race-conditions/server/async-storage.js similarity index 100% rename from exercises/01.exercises/07.solution.module-graph/server/async-storage.js rename to exercises/04.router/03.solution.race-conditions/server/async-storage.js diff --git a/exercises/04.router/03.solution.race-conditions/server/register-rsc-loader.js b/exercises/04.router/03.solution.race-conditions/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/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/04.router/03.solution.race-conditions/server/rsc-loader.js b/exercises/04.router/03.solution.race-conditions/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/04.router/03.solution.race-conditions/tests/playwright.config.js b/exercises/04.router/03.solution.race-conditions/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/04.router/03.solution.race-conditions/tests/race-conditions.test.js b/exercises/04.router/03.solution.race-conditions/tests/race-conditions.test.js new file mode 100644 index 0000000..079fbbb --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/tests/race-conditions.test.js @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test' + +test('should not update URL for out-of-order responses', async ({ page }) => { + await page.goto('/') + + // Simulate varying network delays + await page.route('/rsc/*', async (route) => { + const url = route.request().url() + if (url.includes('search=st')) { + await new Promise((resolve) => setTimeout(resolve, 2000)) // Longer delay for 'st' + } + await route.continue() + }) + + const searchInput = page.getByRole('searchbox', { name: /ships/i }) + + // Perform rapid searches + await searchInput.fill('s') + await searchInput.fill('st') + await searchInput.fill('sta') + + // Wait for the last search to complete + await page.waitForURL('**/*?search=sta') + + // Wait for all in-flight requests to finish + await page.waitForLoadState('networkidle') + + // Check that the URL ends with 'sta', not 'st' + expect(page.url()).toMatch(/search=sta$/) +}) diff --git a/exercises/04.router/03.solution.race-conditions/tests/smoke.test.js b/exercises/04.router/03.solution.race-conditions/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/04.router/03.solution.race-conditions/ui/app.js b/exercises/04.router/03.solution.race-conditions/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/04.router/03.solution.race-conditions/ui/edit-text.js b/exercises/04.router/03.solution.race-conditions/ui/edit-text.js new file mode 100644 index 0000000..b30c19f --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/ui/edit-text.js @@ -0,0 +1,84 @@ +'use client' + +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + null, + edit + ? h( + 'form', + { + onSubmit: (event) => { + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + ) +} diff --git a/exercises/04.router/03.solution.race-conditions/ui/error-boundary.js b/exercises/04.router/03.solution.race-conditions/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/04.router/03.solution.race-conditions/ui/img-utils.js b/exercises/04.router/03.solution.race-conditions/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/04.router/03.solution.race-conditions/ui/img.js b/exercises/04.router/03.solution.race-conditions/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/04.router/03.solution.race-conditions/ui/index.js b/exercises/04.router/03.solution.race-conditions/ui/index.js new file mode 100644 index 0000000..5e500d2 --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/ui/index.js @@ -0,0 +1,101 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + }) +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +function Root() { + const latestNav = useRef(null) + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentPromise, setContentPromise] = useState(initialContentPromise) + const [isPending, startTransition] = useTransition() + + const location = useDeferredValue(nextLocation) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + window.history.replaceState({}, '', nextLocation) + } else { + window.history.pushState({}, '', nextLocation) + } + return response + }), + ) + + startTransition(() => setContentPromise(nextContentPromise)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/04.router/03.solution.race-conditions/ui/router.js b/exercises/04.router/03.solution.race-conditions/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/04.router/03.solution.race-conditions/ui/ship-details-pending.js b/exercises/04.router/03.solution.race-conditions/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/04.router/03.solution.race-conditions/ui/ship-details.js b/exercises/04.router/03.solution.race-conditions/ui/ship-details.js new file mode 100644 index 0000000..5fe0435 --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/ui/ship-details.js @@ -0,0 +1,113 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + initialValue: 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/04.router/03.solution.race-conditions/ui/ship-search-results.js b/exercises/04.router/03.solution.race-conditions/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/04.router/03.solution.race-conditions/ui/ship-search.js b/exercises/04.router/03.solution.race-conditions/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/04.router/03.solution.race-conditions/ui/spin-delay.js b/exercises/04.router/03.solution.race-conditions/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/04.router/03.solution.race-conditions/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/04.router/04.problem.history/.gitignore b/exercises/04.router/04.problem.history/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/04.router/04.problem.history/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/04.router/04.problem.history/README.mdx b/exercises/04.router/04.problem.history/README.mdx new file mode 100644 index 0000000..3c5ea9e --- /dev/null +++ b/exercises/04.router/04.problem.history/README.mdx @@ -0,0 +1,23 @@ +# History + + + +๐Ÿ‘จโ€๐Ÿ’ผ You may have noticed that hitting the back and forward buttons in the browser +does not work. This is because the browser is not aware of the state changes +that are happening in the application. To fix this, we need to add an event +handler to when the browser history changes via the user clicking those buttons. + +The event that's fired when the history changes is called `popstate`. We can +listen for this event and update the application state accordingly. + +```js +function handlePopState() { + // do stuff +} +window.addEventListener('popstate', handlePopState) + +// then to clean up +window.removeEventListener('popstate', handlePopState) +``` + +Please add this event listener in a `useEffect` in our router logic. diff --git a/exercises/04.router/04.problem.history/db/ship-api.js b/exercises/04.router/04.problem.history/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/04.router/04.problem.history/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/04.router/04.problem.history/db/ships.json b/exercises/04.router/04.problem.history/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/04.router/04.problem.history/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/04.router/04.problem.history/package.json b/exercises/04.router/04.problem.history/package.json new file mode 100644 index 0000000..5aed9fb --- /dev/null +++ b/exercises/04.router/04.problem.history/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__04.router__sep__04.problem.history", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/04.router/04.problem.history/public/favicon.ico b/exercises/04.router/04.problem.history/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/04.router/04.problem.history/public/favicon.ico differ diff --git a/exercises/04.router/04.problem.history/public/favicon.svg b/exercises/04.router/04.problem.history/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/04.router/04.problem.history/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/04.router/04.problem.history/public/iframe-sync.js b/exercises/04.router/04.problem.history/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/04.router/04.problem.history/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/04.router/04.problem.history/public/img/broken-ship.webp b/exercises/04.router/04.problem.history/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/broken-ship.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/fallback-ship.png b/exercises/04.router/04.problem.history/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/fallback-ship.png differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/0268fc4817ad1.webp b/exercises/04.router/04.problem.history/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/1ae7b4b92036b.webp b/exercises/04.router/04.problem.history/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/1ff1991efe029.webp b/exercises/04.router/04.problem.history/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/3ba8aa65ffe6c.webp b/exercises/04.router/04.problem.history/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/441f7092a8d44.webp b/exercises/04.router/04.problem.history/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/5c13d8b28a14a.webp b/exercises/04.router/04.problem.history/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/627c497212456.webp b/exercises/04.router/04.problem.history/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/627c497212456.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/670003aed3795.webp b/exercises/04.router/04.problem.history/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/670003aed3795.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/6c86fca8b9086.webp b/exercises/04.router/04.problem.history/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/6f375578ead88.webp b/exercises/04.router/04.problem.history/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/ab267a5984523.webp b/exercises/04.router/04.problem.history/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/b442531ea32b2.webp b/exercises/04.router/04.problem.history/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/bc4cbadf89bd3.webp b/exercises/04.router/04.problem.history/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/cb03cc4e5717e.webp b/exercises/04.router/04.problem.history/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/cfd10fcd2de6c.webp b/exercises/04.router/04.problem.history/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/d3b8aa65ffe6c.webp b/exercises/04.router/04.problem.history/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/d486d48b82b81.webp b/exercises/04.router/04.problem.history/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/e92cefe4f6727.webp b/exercises/04.router/04.problem.history/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/ec7a3f950f99f.webp b/exercises/04.router/04.problem.history/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/f3d9a88e1c234.webp b/exercises/04.router/04.problem.history/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/04.router/04.problem.history/public/img/ships/fdc13cb488bf1.webp b/exercises/04.router/04.problem.history/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/04.router/04.problem.history/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/04.router/04.problem.history/public/index.html b/exercises/04.router/04.problem.history/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/04.router/04.problem.history/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/04.router/04.problem.history/public/style.css b/exercises/04.router/04.problem.history/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/04.router/04.problem.history/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/04.router/04.problem.history/server/app.js b/exercises/04.router/04.problem.history/server/app.js new file mode 100644 index 0000000..c311fef --- /dev/null +++ b/exercises/04.router/04.problem.history/server/app.js @@ -0,0 +1,78 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const moduleBasePath = new URL('../ui', import.meta.url).href + const { pipe } = renderToPipeableStream(h(App), moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/08.problem.hydrate/server/async-storage.js b/exercises/04.router/04.problem.history/server/async-storage.js similarity index 100% rename from exercises/01.exercises/08.problem.hydrate/server/async-storage.js rename to exercises/04.router/04.problem.history/server/async-storage.js diff --git a/exercises/04.router/04.problem.history/server/register-rsc-loader.js b/exercises/04.router/04.problem.history/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/04.router/04.problem.history/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/04.router/04.problem.history/server/rsc-loader.js b/exercises/04.router/04.problem.history/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/04.router/04.problem.history/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/04.router/04.problem.history/tests/history.test.js b/exercises/04.router/04.problem.history/tests/history.test.js new file mode 100644 index 0000000..e587554 --- /dev/null +++ b/exercises/04.router/04.problem.history/tests/history.test.js @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test' + +test('going forward and backward in history updates the UI', async ({ + page, +}) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + + const shipList = page.getByRole('list').first() + + // Click the first item and store its text content + const firstShipLink = shipList.getByRole('link').first() + const firstShipName = await firstShipLink.textContent() + await firstShipLink.click() + + // Wait for the h2 heading to update and verify its content + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(firstShipName) + + // Click the second item and store its text content + const secondShipLink = shipList.getByRole('link').nth(1) + const secondShipName = await secondShipLink.textContent() + await secondShipLink.click() + + // Wait for the h2 heading to update and verify its content + await expect(shipTitle).toHaveText(secondShipName) + + // Go back in browser history + await page.goBack() + + // Verify the h2 heading is set back to the first ship's name + await expect(shipTitle).toHaveText(firstShipName) + + // Go forward in browser history + await page.goForward() + + // Verify the h2 heading is set back to the second ship's name + await expect(shipTitle).toHaveText(secondShipName) +}) diff --git a/exercises/04.router/04.problem.history/tests/playwright.config.js b/exercises/04.router/04.problem.history/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/04.router/04.problem.history/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/04.router/04.problem.history/tests/smoke.test.js b/exercises/04.router/04.problem.history/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/04.router/04.problem.history/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/04.router/04.problem.history/ui/app.js b/exercises/04.router/04.problem.history/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/04.router/04.problem.history/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/04.router/04.problem.history/ui/edit-text.js b/exercises/04.router/04.problem.history/ui/edit-text.js new file mode 100644 index 0000000..b30c19f --- /dev/null +++ b/exercises/04.router/04.problem.history/ui/edit-text.js @@ -0,0 +1,84 @@ +'use client' + +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + null, + edit + ? h( + 'form', + { + onSubmit: (event) => { + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + ) +} diff --git a/exercises/04.router/04.problem.history/ui/error-boundary.js b/exercises/04.router/04.problem.history/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/04.router/04.problem.history/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/04.router/04.problem.history/ui/img-utils.js b/exercises/04.router/04.problem.history/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/04.router/04.problem.history/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/04.router/04.problem.history/ui/img.js b/exercises/04.router/04.problem.history/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/04.router/04.problem.history/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/04.router/04.problem.history/ui/index.js b/exercises/04.router/04.problem.history/ui/index.js new file mode 100644 index 0000000..c82fd02 --- /dev/null +++ b/exercises/04.router/04.problem.history/ui/index.js @@ -0,0 +1,113 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + // ๐Ÿ’ฐ you're gonna need this + // useEffect, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + }) +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +function Root() { + const latestNav = useRef(null) + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentPromise, setContentPromise] = useState(initialContentPromise) + const [isPending, startTransition] = useTransition() + + const location = useDeferredValue(nextLocation) + + // ๐Ÿจ add a useEffect here to add a popstate listener + // ๐Ÿจ make a handlePopState function + // - create a nextLocation variable set to getGlobalLocation() + // - call setNextLocation with that nextLocation + // - fetchContent for that nextLocation and set it to fetchPromise + // - create a nextContentPromise using createFromFetch(fetchPromise) + // - start an transition that sets the contentPromise to nextContentPromise + // ๐Ÿจ add that handlePopState as an event listener to the popstate event on window + // ๐Ÿจ don't forget to remove the event listener in the cleanup! + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + window.history.replaceState({}, '', nextLocation) + } else { + window.history.pushState({}, '', nextLocation) + } + return response + }), + ) + + startTransition(() => setContentPromise(nextContentPromise)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/04.router/04.problem.history/ui/router.js b/exercises/04.router/04.problem.history/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/04.router/04.problem.history/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/04.router/04.problem.history/ui/ship-details-pending.js b/exercises/04.router/04.problem.history/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/04.router/04.problem.history/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/04.router/04.problem.history/ui/ship-details.js b/exercises/04.router/04.problem.history/ui/ship-details.js new file mode 100644 index 0000000..5fe0435 --- /dev/null +++ b/exercises/04.router/04.problem.history/ui/ship-details.js @@ -0,0 +1,113 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + initialValue: 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/04.router/04.problem.history/ui/ship-search-results.js b/exercises/04.router/04.problem.history/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/04.router/04.problem.history/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/04.router/04.problem.history/ui/ship-search.js b/exercises/04.router/04.problem.history/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/04.router/04.problem.history/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/04.router/04.problem.history/ui/spin-delay.js b/exercises/04.router/04.problem.history/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/04.router/04.problem.history/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/04.router/04.solution.history/.gitignore b/exercises/04.router/04.solution.history/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/04.router/04.solution.history/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/04.router/04.solution.history/README.mdx b/exercises/04.router/04.solution.history/README.mdx new file mode 100644 index 0000000..2c89258 --- /dev/null +++ b/exercises/04.router/04.solution.history/README.mdx @@ -0,0 +1,6 @@ +# History + + + +๐Ÿ‘จโ€๐Ÿ’ผ Stellar! Now when we hit forward and back we get the UI updating. But oddly +we're getting our suspense fallbacks rendering. Let's handle that next. diff --git a/exercises/04.router/04.solution.history/db/ship-api.js b/exercises/04.router/04.solution.history/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/04.router/04.solution.history/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/04.router/04.solution.history/db/ships.json b/exercises/04.router/04.solution.history/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/04.router/04.solution.history/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/04.router/04.solution.history/package.json b/exercises/04.router/04.solution.history/package.json new file mode 100644 index 0000000..ffc7df1 --- /dev/null +++ b/exercises/04.router/04.solution.history/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__04.router__sep__04.solution.history", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/04.router/04.solution.history/public/favicon.ico b/exercises/04.router/04.solution.history/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/04.router/04.solution.history/public/favicon.ico differ diff --git a/exercises/04.router/04.solution.history/public/favicon.svg b/exercises/04.router/04.solution.history/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/04.router/04.solution.history/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/04.router/04.solution.history/public/iframe-sync.js b/exercises/04.router/04.solution.history/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/04.router/04.solution.history/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/04.router/04.solution.history/public/img/broken-ship.webp b/exercises/04.router/04.solution.history/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/broken-ship.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/fallback-ship.png b/exercises/04.router/04.solution.history/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/fallback-ship.png differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/0268fc4817ad1.webp b/exercises/04.router/04.solution.history/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/1ae7b4b92036b.webp b/exercises/04.router/04.solution.history/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/1ff1991efe029.webp b/exercises/04.router/04.solution.history/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/3ba8aa65ffe6c.webp b/exercises/04.router/04.solution.history/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/441f7092a8d44.webp b/exercises/04.router/04.solution.history/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/5c13d8b28a14a.webp b/exercises/04.router/04.solution.history/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/627c497212456.webp b/exercises/04.router/04.solution.history/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/627c497212456.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/670003aed3795.webp b/exercises/04.router/04.solution.history/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/670003aed3795.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/6c86fca8b9086.webp b/exercises/04.router/04.solution.history/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/6f375578ead88.webp b/exercises/04.router/04.solution.history/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/ab267a5984523.webp b/exercises/04.router/04.solution.history/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/b442531ea32b2.webp b/exercises/04.router/04.solution.history/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/bc4cbadf89bd3.webp b/exercises/04.router/04.solution.history/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/cb03cc4e5717e.webp b/exercises/04.router/04.solution.history/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/cfd10fcd2de6c.webp b/exercises/04.router/04.solution.history/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/d3b8aa65ffe6c.webp b/exercises/04.router/04.solution.history/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/d486d48b82b81.webp b/exercises/04.router/04.solution.history/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/e92cefe4f6727.webp b/exercises/04.router/04.solution.history/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/ec7a3f950f99f.webp b/exercises/04.router/04.solution.history/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/f3d9a88e1c234.webp b/exercises/04.router/04.solution.history/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/04.router/04.solution.history/public/img/ships/fdc13cb488bf1.webp b/exercises/04.router/04.solution.history/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/04.router/04.solution.history/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/04.router/04.solution.history/public/index.html b/exercises/04.router/04.solution.history/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/04.router/04.solution.history/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/04.router/04.solution.history/public/style.css b/exercises/04.router/04.solution.history/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/04.router/04.solution.history/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/04.router/04.solution.history/server/app.js b/exercises/04.router/04.solution.history/server/app.js new file mode 100644 index 0000000..c311fef --- /dev/null +++ b/exercises/04.router/04.solution.history/server/app.js @@ -0,0 +1,78 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const moduleBasePath = new URL('../ui', import.meta.url).href + const { pipe } = renderToPipeableStream(h(App), moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/08.solution.hydrate/server/async-storage.js b/exercises/04.router/04.solution.history/server/async-storage.js similarity index 100% rename from exercises/01.exercises/08.solution.hydrate/server/async-storage.js rename to exercises/04.router/04.solution.history/server/async-storage.js diff --git a/exercises/04.router/04.solution.history/server/register-rsc-loader.js b/exercises/04.router/04.solution.history/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/04.router/04.solution.history/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/04.router/04.solution.history/server/rsc-loader.js b/exercises/04.router/04.solution.history/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/04.router/04.solution.history/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/04.router/04.solution.history/tests/history.test.js b/exercises/04.router/04.solution.history/tests/history.test.js new file mode 100644 index 0000000..e587554 --- /dev/null +++ b/exercises/04.router/04.solution.history/tests/history.test.js @@ -0,0 +1,39 @@ +import { test, expect } from '@playwright/test' + +test('going forward and backward in history updates the UI', async ({ + page, +}) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + + const shipList = page.getByRole('list').first() + + // Click the first item and store its text content + const firstShipLink = shipList.getByRole('link').first() + const firstShipName = await firstShipLink.textContent() + await firstShipLink.click() + + // Wait for the h2 heading to update and verify its content + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(firstShipName) + + // Click the second item and store its text content + const secondShipLink = shipList.getByRole('link').nth(1) + const secondShipName = await secondShipLink.textContent() + await secondShipLink.click() + + // Wait for the h2 heading to update and verify its content + await expect(shipTitle).toHaveText(secondShipName) + + // Go back in browser history + await page.goBack() + + // Verify the h2 heading is set back to the first ship's name + await expect(shipTitle).toHaveText(firstShipName) + + // Go forward in browser history + await page.goForward() + + // Verify the h2 heading is set back to the second ship's name + await expect(shipTitle).toHaveText(secondShipName) +}) diff --git a/exercises/04.router/04.solution.history/tests/playwright.config.js b/exercises/04.router/04.solution.history/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/04.router/04.solution.history/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/04.router/04.solution.history/tests/smoke.test.js b/exercises/04.router/04.solution.history/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/04.router/04.solution.history/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/04.router/04.solution.history/ui/app.js b/exercises/04.router/04.solution.history/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/04.router/04.solution.history/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/04.router/04.solution.history/ui/edit-text.js b/exercises/04.router/04.solution.history/ui/edit-text.js new file mode 100644 index 0000000..b30c19f --- /dev/null +++ b/exercises/04.router/04.solution.history/ui/edit-text.js @@ -0,0 +1,84 @@ +'use client' + +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + null, + edit + ? h( + 'form', + { + onSubmit: (event) => { + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + ) +} diff --git a/exercises/04.router/04.solution.history/ui/error-boundary.js b/exercises/04.router/04.solution.history/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/04.router/04.solution.history/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/04.router/04.solution.history/ui/img-utils.js b/exercises/04.router/04.solution.history/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/04.router/04.solution.history/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/04.router/04.solution.history/ui/img.js b/exercises/04.router/04.solution.history/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/04.router/04.solution.history/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/04.router/04.solution.history/ui/index.js b/exercises/04.router/04.solution.history/ui/index.js new file mode 100644 index 0000000..7d417de --- /dev/null +++ b/exercises/04.router/04.solution.history/ui/index.js @@ -0,0 +1,116 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useEffect, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + }) +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +function Root() { + const latestNav = useRef(null) + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentPromise, setContentPromise] = useState(initialContentPromise) + const [isPending, startTransition] = useTransition() + + const location = useDeferredValue(nextLocation) + + useEffect(() => { + function handlePopState() { + const nextLocation = getGlobalLocation() + setNextLocation(nextLocation) + + const fetchPromise = fetchContent(nextLocation) + const nextContentPromise = createFromFetch(fetchPromise) + + startTransition(() => setContentPromise(nextContentPromise)) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, []) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + window.history.replaceState({}, '', nextLocation) + } else { + window.history.pushState({}, '', nextLocation) + } + return response + }), + ) + + startTransition(() => setContentPromise(nextContentPromise)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/04.router/04.solution.history/ui/router.js b/exercises/04.router/04.solution.history/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/04.router/04.solution.history/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/04.router/04.solution.history/ui/ship-details-pending.js b/exercises/04.router/04.solution.history/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/04.router/04.solution.history/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/04.router/04.solution.history/ui/ship-details.js b/exercises/04.router/04.solution.history/ui/ship-details.js new file mode 100644 index 0000000..5fe0435 --- /dev/null +++ b/exercises/04.router/04.solution.history/ui/ship-details.js @@ -0,0 +1,113 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + initialValue: 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/04.router/04.solution.history/ui/ship-search-results.js b/exercises/04.router/04.solution.history/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/04.router/04.solution.history/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/04.router/04.solution.history/ui/ship-search.js b/exercises/04.router/04.solution.history/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/04.router/04.solution.history/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/04.router/04.solution.history/ui/spin-delay.js b/exercises/04.router/04.solution.history/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/04.router/04.solution.history/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/04.router/05.problem.cache/.gitignore b/exercises/04.router/05.problem.cache/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/04.router/05.problem.cache/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/04.router/05.problem.cache/README.mdx b/exercises/04.router/05.problem.cache/README.mdx new file mode 100644 index 0000000..f838bda --- /dev/null +++ b/exercises/04.router/05.problem.cache/README.mdx @@ -0,0 +1,50 @@ +# Cache + + + +๐Ÿ‘จโ€๐Ÿ’ผ When we hit the back/forward buttons, we're getting our suspense boundaries +for a bit. This is confusing because we've wrapped our content state update +in `startTransition` which should keep the old UI around until the new one is +ready. + +This is actually intentional by the React team. They force suspense boundaries +to be shown when we hit the back/forward buttons if anything suspends to manage +things like scroll position and focus more accurately. + +So what do we do? Well, we need to make it so our component doesn't suspend. We +do this by caching the content as the user navigates around, that way when they +go back/forward, we can just show the cached content. + +The trick is, what should we use as the cache key? We can't use the URL because +the user could have navigated to the same URL multiple times in their history +and the content is not necessarily the same. + +The `window.history` API actually supports client-side `state` which the browser +will manage for us: + +```js +window.history.pushState({ key: 'some-unique-key' }, '', '/some-url') + +// then later +const key = window.history.state?.key +``` + +This is perfect. So we can simply generate a random ID, store the content +promise in a cache, then store the `contentKey` in state instead of the +`contentPromise` directly. Then, whenever we navigate, or when `popstate` events +happen, we can create a new `contentPromise` and update the cache and cache key. + +๐Ÿงโ€โ™‚๏ธ I've implemented a special map that allows us to track whenever values change +in it so React will rerender our app when we update the cache. So you can get +the `contentCache` directly from for +use in the module, but within the `Root` component, you can use the +`useContentCache()` hook to get a cache object that will trigger rerenders when +you update it. + +๐Ÿ‘จโ€๐Ÿ’ผ Great, thanks Kellie. Another thing you're going to want to do is make sure +when we land on the page, if the history doesn't already have a key, we generate +one, replace history with that key and get the content promise into the cache. + +And in the `popstate` case, if the key is already in the cache, we can just +update the `contentKey` in state and let the component rerender with the cached +content rather than fetching new content. diff --git a/exercises/04.router/05.problem.cache/db/ship-api.js b/exercises/04.router/05.problem.cache/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/04.router/05.problem.cache/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/04.router/05.problem.cache/db/ships.json b/exercises/04.router/05.problem.cache/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/04.router/05.problem.cache/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/04.router/05.problem.cache/package.json b/exercises/04.router/05.problem.cache/package.json new file mode 100644 index 0000000..7cf67e7 --- /dev/null +++ b/exercises/04.router/05.problem.cache/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__04.router__sep__05.problem.cache", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/04.router/05.problem.cache/public/favicon.ico b/exercises/04.router/05.problem.cache/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/favicon.ico differ diff --git a/exercises/04.router/05.problem.cache/public/favicon.svg b/exercises/04.router/05.problem.cache/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/04.router/05.problem.cache/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/04.router/05.problem.cache/public/iframe-sync.js b/exercises/04.router/05.problem.cache/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/04.router/05.problem.cache/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/04.router/05.problem.cache/public/img/broken-ship.webp b/exercises/04.router/05.problem.cache/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/broken-ship.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/fallback-ship.png b/exercises/04.router/05.problem.cache/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/fallback-ship.png differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/0268fc4817ad1.webp b/exercises/04.router/05.problem.cache/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/1ae7b4b92036b.webp b/exercises/04.router/05.problem.cache/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/1ff1991efe029.webp b/exercises/04.router/05.problem.cache/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/3ba8aa65ffe6c.webp b/exercises/04.router/05.problem.cache/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/441f7092a8d44.webp b/exercises/04.router/05.problem.cache/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/5c13d8b28a14a.webp b/exercises/04.router/05.problem.cache/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/627c497212456.webp b/exercises/04.router/05.problem.cache/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/627c497212456.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/670003aed3795.webp b/exercises/04.router/05.problem.cache/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/670003aed3795.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/6c86fca8b9086.webp b/exercises/04.router/05.problem.cache/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/6f375578ead88.webp b/exercises/04.router/05.problem.cache/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/ab267a5984523.webp b/exercises/04.router/05.problem.cache/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/b442531ea32b2.webp b/exercises/04.router/05.problem.cache/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/bc4cbadf89bd3.webp b/exercises/04.router/05.problem.cache/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/cb03cc4e5717e.webp b/exercises/04.router/05.problem.cache/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/cfd10fcd2de6c.webp b/exercises/04.router/05.problem.cache/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/d3b8aa65ffe6c.webp b/exercises/04.router/05.problem.cache/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/d486d48b82b81.webp b/exercises/04.router/05.problem.cache/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/e92cefe4f6727.webp b/exercises/04.router/05.problem.cache/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/ec7a3f950f99f.webp b/exercises/04.router/05.problem.cache/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/f3d9a88e1c234.webp b/exercises/04.router/05.problem.cache/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/04.router/05.problem.cache/public/img/ships/fdc13cb488bf1.webp b/exercises/04.router/05.problem.cache/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/04.router/05.problem.cache/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/04.router/05.problem.cache/public/index.html b/exercises/04.router/05.problem.cache/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/04.router/05.problem.cache/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/04.router/05.problem.cache/public/style.css b/exercises/04.router/05.problem.cache/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/04.router/05.problem.cache/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/04.router/05.problem.cache/server/app.js b/exercises/04.router/05.problem.cache/server/app.js new file mode 100644 index 0000000..c311fef --- /dev/null +++ b/exercises/04.router/05.problem.cache/server/app.js @@ -0,0 +1,78 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const moduleBasePath = new URL('../ui', import.meta.url).href + const { pipe } = renderToPipeableStream(h(App), moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/09.problem.routing/server/async-storage.js b/exercises/04.router/05.problem.cache/server/async-storage.js similarity index 100% rename from exercises/01.exercises/09.problem.routing/server/async-storage.js rename to exercises/04.router/05.problem.cache/server/async-storage.js diff --git a/exercises/04.router/05.problem.cache/server/register-rsc-loader.js b/exercises/04.router/05.problem.cache/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/04.router/05.problem.cache/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/04.router/05.problem.cache/server/rsc-loader.js b/exercises/04.router/05.problem.cache/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/04.router/05.problem.cache/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/04.router/05.problem.cache/tests/cache.test.js b/exercises/04.router/05.problem.cache/tests/cache.test.js new file mode 100644 index 0000000..594b3d3 --- /dev/null +++ b/exercises/04.router/05.problem.cache/tests/cache.test.js @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test' +import { shipFallbackSrc } from '../ui/img-utils.js' + +test('going forward and backward in history updates the UI', async ({ + page, +}) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + + // simulate a slow network for the /rsc endpoint so we force the pending UI to show up + await page.route('/rsc/*', async (route) => { + await new Promise((resolve) => + setTimeout(resolve, process.env.CI ? 1000 : 400), + ) + await route.continue() + }) + + const shipList = page.getByRole('list').first() + + // Click the first item and store its text content + const firstShipLink = shipList.getByRole('link').first() + const firstShipName = await firstShipLink.textContent() + await firstShipLink.click() + + // Wait for the h2 heading to update and verify its content + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(firstShipName) + + // Click the second item and store its text content + const secondShipLink = shipList.getByRole('link').nth(1) + const secondShipName = await secondShipLink.textContent() + await secondShipLink.click() + + // Wait for the h2 heading to update and verify its content + await expect(shipTitle).toHaveText(secondShipName) + + // Go back in browser history + await page.goBack() + // Verify the root suspense boundary is not displayed. + await expect(page.locator(`img[src="${shipFallbackSrc}"]`)).not.toBeVisible() + + // Verify the h2 heading is set back to the first ship's name + await expect(shipTitle).toHaveText(firstShipName) + + // Go forward in browser history + await page.goForward() + // Verify the root suspense boundary is not displayed. + await expect(page.locator(`img[src="${shipFallbackSrc}"]`)).not.toBeVisible() + + // Verify the h2 heading is set back to the second ship's name + await expect(shipTitle).toHaveText(secondShipName) +}) diff --git a/exercises/04.router/05.problem.cache/tests/playwright.config.js b/exercises/04.router/05.problem.cache/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/04.router/05.problem.cache/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/04.router/05.problem.cache/tests/smoke.test.js b/exercises/04.router/05.problem.cache/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/04.router/05.problem.cache/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/04.router/05.problem.cache/ui/app.js b/exercises/04.router/05.problem.cache/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/04.router/05.problem.cache/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/04.router/05.problem.cache/ui/content-cache.js b/exercises/04.router/05.problem.cache/ui/content-cache.js new file mode 100644 index 0000000..612b97f --- /dev/null +++ b/exercises/04.router/05.problem.cache/ui/content-cache.js @@ -0,0 +1,46 @@ +import { useSyncExternalStore } from 'react' + +/** + * This will call the given callback function whenever the contents of the map + * change. + */ +class ObservableMap extends Map { + listeners = new Set() + set(key, value) { + const result = super.set(key, value) + this.emitChange() + return result + } + delete(key) { + const result = super.delete(key) + this.emitChange() + return result + } + emitChange() { + for (const listener of this.listeners) { + listener() + } + } + subscribe(listener) { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } +} + +export const contentCache = new ObservableMap() + +export function generateKey() { + return Date.now().toString(36) + Math.random().toString(36).slice(2) +} + +export function useContentCache() { + function subscribe(cb) { + return contentCache.subscribe(cb) + } + function getSnapshot() { + return contentCache + } + return useSyncExternalStore(subscribe, getSnapshot) +} diff --git a/exercises/04.router/05.problem.cache/ui/edit-text.js b/exercises/04.router/05.problem.cache/ui/edit-text.js new file mode 100644 index 0000000..b30c19f --- /dev/null +++ b/exercises/04.router/05.problem.cache/ui/edit-text.js @@ -0,0 +1,84 @@ +'use client' + +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + null, + edit + ? h( + 'form', + { + onSubmit: (event) => { + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + ) +} diff --git a/exercises/04.router/05.problem.cache/ui/error-boundary.js b/exercises/04.router/05.problem.cache/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/04.router/05.problem.cache/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/04.router/05.problem.cache/ui/img-utils.js b/exercises/04.router/05.problem.cache/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/04.router/05.problem.cache/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/04.router/05.problem.cache/ui/img.js b/exercises/04.router/05.problem.cache/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/04.router/05.problem.cache/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/04.router/05.problem.cache/ui/index.js b/exercises/04.router/05.problem.cache/ui/index.js new file mode 100644 index 0000000..d198c70 --- /dev/null +++ b/exercises/04.router/05.problem.cache/ui/index.js @@ -0,0 +1,138 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useEffect, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +// ๐Ÿ’ฐ you're going to need this +// import { contentCache, useContentCache, generateKey } from './content-cache.js' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + }) +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +// ๐Ÿจ create an initialContentKey here assigned to window.history.state?.key +// ๐Ÿจ if there's no initialContentKey +// - set it to a new generated one with generateKey +// - call window.history.replaceState with the initialContentKey + +// ๐Ÿจ use the initialContentKey to add the initialContentPromise in the contentCache + +function Root() { + const latestNav = useRef(null) + // ๐Ÿจ get the contentCache from useContentCache + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + // ๐Ÿจ change this to contentKey + const [contentPromise, setContentPromise] = useState(initialContentPromise) + const [isPending, startTransition] = useTransition() + + const location = useDeferredValue(nextLocation) + // ๐Ÿจ get the contentPromise from the contentCache by the contentKey + + useEffect(() => { + function handlePopState() { + const nextLocation = getGlobalLocation() + setNextLocation(nextLocation) + // ๐Ÿจ get the historyKey from window.history.state?.key (or fallback to a new one with generateKey) + + // ๐Ÿจ if the contentCache does not have an entry for the historyKey, then trigger this update: + const fetchPromise = fetchContent(nextLocation) + const nextContentPromise = createFromFetch(fetchPromise) + // ๐Ÿจ use the historyKey to add the nextContentPromise in the contentCache + + // ๐Ÿจ change this to setContentKey(historyKey) + startTransition(() => setContentPromise(nextContentPromise)) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, []) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + // ๐Ÿจ create a nextContentKey with generateKey() + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + // ๐Ÿจ add a key property here + window.history.replaceState({}, '', nextLocation) + } else { + // ๐Ÿจ add a key property here + window.history.pushState({}, '', nextLocation) + } + return response + }), + ) + + // ๐Ÿจ use the nextContentKey to add the nextContentPromise in the contentCache + + // ๐Ÿจ update this to setContentKey(newContentKey) + startTransition(() => setContentPromise(nextContentPromise)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/04.router/05.problem.cache/ui/router.js b/exercises/04.router/05.problem.cache/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/04.router/05.problem.cache/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/04.router/05.problem.cache/ui/ship-details-pending.js b/exercises/04.router/05.problem.cache/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/04.router/05.problem.cache/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/04.router/05.problem.cache/ui/ship-details.js b/exercises/04.router/05.problem.cache/ui/ship-details.js new file mode 100644 index 0000000..5fe0435 --- /dev/null +++ b/exercises/04.router/05.problem.cache/ui/ship-details.js @@ -0,0 +1,113 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + initialValue: 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/04.router/05.problem.cache/ui/ship-search-results.js b/exercises/04.router/05.problem.cache/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/04.router/05.problem.cache/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/04.router/05.problem.cache/ui/ship-search.js b/exercises/04.router/05.problem.cache/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/04.router/05.problem.cache/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/04.router/05.problem.cache/ui/spin-delay.js b/exercises/04.router/05.problem.cache/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/04.router/05.problem.cache/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/04.router/05.solution.cache/.gitignore b/exercises/04.router/05.solution.cache/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/04.router/05.solution.cache/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/04.router/05.solution.cache/README.mdx b/exercises/04.router/05.solution.cache/README.mdx new file mode 100644 index 0000000..aafeed0 --- /dev/null +++ b/exercises/04.router/05.solution.cache/README.mdx @@ -0,0 +1,6 @@ +# Cache + + + +๐Ÿ‘จโ€๐Ÿ’ผ Hooray! Now we can go back and forward without any delay. This looks much +better! Thanks. diff --git a/exercises/04.router/05.solution.cache/db/ship-api.js b/exercises/04.router/05.solution.cache/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/04.router/05.solution.cache/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/04.router/05.solution.cache/db/ships.json b/exercises/04.router/05.solution.cache/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/04.router/05.solution.cache/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/04.router/05.solution.cache/package.json b/exercises/04.router/05.solution.cache/package.json new file mode 100644 index 0000000..51a1ddc --- /dev/null +++ b/exercises/04.router/05.solution.cache/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__04.router__sep__05.solution.cache", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/04.router/05.solution.cache/public/favicon.ico b/exercises/04.router/05.solution.cache/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/favicon.ico differ diff --git a/exercises/04.router/05.solution.cache/public/favicon.svg b/exercises/04.router/05.solution.cache/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/04.router/05.solution.cache/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/04.router/05.solution.cache/public/iframe-sync.js b/exercises/04.router/05.solution.cache/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/04.router/05.solution.cache/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/04.router/05.solution.cache/public/img/broken-ship.webp b/exercises/04.router/05.solution.cache/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/broken-ship.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/fallback-ship.png b/exercises/04.router/05.solution.cache/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/fallback-ship.png differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/0268fc4817ad1.webp b/exercises/04.router/05.solution.cache/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/1ae7b4b92036b.webp b/exercises/04.router/05.solution.cache/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/1ff1991efe029.webp b/exercises/04.router/05.solution.cache/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/3ba8aa65ffe6c.webp b/exercises/04.router/05.solution.cache/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/441f7092a8d44.webp b/exercises/04.router/05.solution.cache/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/5c13d8b28a14a.webp b/exercises/04.router/05.solution.cache/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/627c497212456.webp b/exercises/04.router/05.solution.cache/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/627c497212456.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/670003aed3795.webp b/exercises/04.router/05.solution.cache/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/670003aed3795.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/6c86fca8b9086.webp b/exercises/04.router/05.solution.cache/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/6f375578ead88.webp b/exercises/04.router/05.solution.cache/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/ab267a5984523.webp b/exercises/04.router/05.solution.cache/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/b442531ea32b2.webp b/exercises/04.router/05.solution.cache/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/bc4cbadf89bd3.webp b/exercises/04.router/05.solution.cache/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/cb03cc4e5717e.webp b/exercises/04.router/05.solution.cache/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/cfd10fcd2de6c.webp b/exercises/04.router/05.solution.cache/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/d3b8aa65ffe6c.webp b/exercises/04.router/05.solution.cache/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/d486d48b82b81.webp b/exercises/04.router/05.solution.cache/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/e92cefe4f6727.webp b/exercises/04.router/05.solution.cache/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/ec7a3f950f99f.webp b/exercises/04.router/05.solution.cache/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/f3d9a88e1c234.webp b/exercises/04.router/05.solution.cache/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/04.router/05.solution.cache/public/img/ships/fdc13cb488bf1.webp b/exercises/04.router/05.solution.cache/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/04.router/05.solution.cache/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/04.router/05.solution.cache/public/index.html b/exercises/04.router/05.solution.cache/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/04.router/05.solution.cache/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/04.router/05.solution.cache/public/style.css b/exercises/04.router/05.solution.cache/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/04.router/05.solution.cache/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/04.router/05.solution.cache/server/app.js b/exercises/04.router/05.solution.cache/server/app.js new file mode 100644 index 0000000..c311fef --- /dev/null +++ b/exercises/04.router/05.solution.cache/server/app.js @@ -0,0 +1,78 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +app.get('/rsc/:shipId?', async (context) => { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const moduleBasePath = new URL('../ui', import.meta.url).href + const { pipe } = renderToPipeableStream(h(App), moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/09.solution.routing/server/async-storage.js b/exercises/04.router/05.solution.cache/server/async-storage.js similarity index 100% rename from exercises/01.exercises/09.solution.routing/server/async-storage.js rename to exercises/04.router/05.solution.cache/server/async-storage.js diff --git a/exercises/04.router/05.solution.cache/server/register-rsc-loader.js b/exercises/04.router/05.solution.cache/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/04.router/05.solution.cache/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/04.router/05.solution.cache/server/rsc-loader.js b/exercises/04.router/05.solution.cache/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/04.router/05.solution.cache/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/04.router/05.solution.cache/tests/cache.test.js b/exercises/04.router/05.solution.cache/tests/cache.test.js new file mode 100644 index 0000000..594b3d3 --- /dev/null +++ b/exercises/04.router/05.solution.cache/tests/cache.test.js @@ -0,0 +1,52 @@ +import { test, expect } from '@playwright/test' +import { shipFallbackSrc } from '../ui/img-utils.js' + +test('going forward and backward in history updates the UI', async ({ + page, +}) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + + // simulate a slow network for the /rsc endpoint so we force the pending UI to show up + await page.route('/rsc/*', async (route) => { + await new Promise((resolve) => + setTimeout(resolve, process.env.CI ? 1000 : 400), + ) + await route.continue() + }) + + const shipList = page.getByRole('list').first() + + // Click the first item and store its text content + const firstShipLink = shipList.getByRole('link').first() + const firstShipName = await firstShipLink.textContent() + await firstShipLink.click() + + // Wait for the h2 heading to update and verify its content + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(firstShipName) + + // Click the second item and store its text content + const secondShipLink = shipList.getByRole('link').nth(1) + const secondShipName = await secondShipLink.textContent() + await secondShipLink.click() + + // Wait for the h2 heading to update and verify its content + await expect(shipTitle).toHaveText(secondShipName) + + // Go back in browser history + await page.goBack() + // Verify the root suspense boundary is not displayed. + await expect(page.locator(`img[src="${shipFallbackSrc}"]`)).not.toBeVisible() + + // Verify the h2 heading is set back to the first ship's name + await expect(shipTitle).toHaveText(firstShipName) + + // Go forward in browser history + await page.goForward() + // Verify the root suspense boundary is not displayed. + await expect(page.locator(`img[src="${shipFallbackSrc}"]`)).not.toBeVisible() + + // Verify the h2 heading is set back to the second ship's name + await expect(shipTitle).toHaveText(secondShipName) +}) diff --git a/exercises/04.router/05.solution.cache/tests/playwright.config.js b/exercises/04.router/05.solution.cache/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/04.router/05.solution.cache/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/04.router/05.solution.cache/tests/smoke.test.js b/exercises/04.router/05.solution.cache/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/04.router/05.solution.cache/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/04.router/05.solution.cache/ui/app.js b/exercises/04.router/05.solution.cache/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/04.router/05.solution.cache/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/04.router/05.solution.cache/ui/content-cache.js b/exercises/04.router/05.solution.cache/ui/content-cache.js new file mode 100644 index 0000000..612b97f --- /dev/null +++ b/exercises/04.router/05.solution.cache/ui/content-cache.js @@ -0,0 +1,46 @@ +import { useSyncExternalStore } from 'react' + +/** + * This will call the given callback function whenever the contents of the map + * change. + */ +class ObservableMap extends Map { + listeners = new Set() + set(key, value) { + const result = super.set(key, value) + this.emitChange() + return result + } + delete(key) { + const result = super.delete(key) + this.emitChange() + return result + } + emitChange() { + for (const listener of this.listeners) { + listener() + } + } + subscribe(listener) { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } +} + +export const contentCache = new ObservableMap() + +export function generateKey() { + return Date.now().toString(36) + Math.random().toString(36).slice(2) +} + +export function useContentCache() { + function subscribe(cb) { + return contentCache.subscribe(cb) + } + function getSnapshot() { + return contentCache + } + return useSyncExternalStore(subscribe, getSnapshot) +} diff --git a/exercises/04.router/05.solution.cache/ui/edit-text.js b/exercises/04.router/05.solution.cache/ui/edit-text.js new file mode 100644 index 0000000..b30c19f --- /dev/null +++ b/exercises/04.router/05.solution.cache/ui/edit-text.js @@ -0,0 +1,84 @@ +'use client' + +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + null, + edit + ? h( + 'form', + { + onSubmit: (event) => { + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + ) +} diff --git a/exercises/04.router/05.solution.cache/ui/error-boundary.js b/exercises/04.router/05.solution.cache/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/04.router/05.solution.cache/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/04.router/05.solution.cache/ui/img-utils.js b/exercises/04.router/05.solution.cache/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/04.router/05.solution.cache/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/04.router/05.solution.cache/ui/img.js b/exercises/04.router/05.solution.cache/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/04.router/05.solution.cache/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/04.router/05.solution.cache/ui/index.js b/exercises/04.router/05.solution.cache/ui/index.js new file mode 100644 index 0000000..86d7077 --- /dev/null +++ b/exercises/04.router/05.solution.cache/ui/index.js @@ -0,0 +1,132 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useEffect, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { contentCache, useContentCache, generateKey } from './content-cache.js' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + }) +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +let initialContentKey = window.history.state?.key +if (!initialContentKey) { + initialContentKey = generateKey() + window.history.replaceState({ key: initialContentKey }, '') +} +contentCache.set(initialContentKey, initialContentPromise) + +function Root() { + const latestNav = useRef(null) + const contentCache = useContentCache() + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentKey, setContentKey] = useState(initialContentKey) + const [isPending, startTransition] = useTransition() + + const location = useDeferredValue(nextLocation) + const contentPromise = contentCache.get(contentKey) + + useEffect(() => { + function handlePopState() { + const nextLocation = getGlobalLocation() + setNextLocation(nextLocation) + const historyKey = window.history.state?.key ?? generateKey() + + if (!contentCache.has(historyKey)) { + const fetchPromise = fetchContent(nextLocation) + const nextContentPromise = createFromFetch(fetchPromise) + contentCache.set(historyKey, nextContentPromise) + } + + startTransition(() => setContentKey(historyKey)) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, [contentCache]) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + const newContentKey = generateKey() + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + window.history.replaceState({ key: newContentKey }, '', nextLocation) + } else { + window.history.pushState({ key: newContentKey }, '', nextLocation) + } + return response + }), + ) + + contentCache.set(newContentKey, nextContentPromise) + startTransition(() => setContentKey(newContentKey)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/04.router/05.solution.cache/ui/router.js b/exercises/04.router/05.solution.cache/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/04.router/05.solution.cache/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/04.router/05.solution.cache/ui/ship-details-pending.js b/exercises/04.router/05.solution.cache/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/04.router/05.solution.cache/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/04.router/05.solution.cache/ui/ship-details.js b/exercises/04.router/05.solution.cache/ui/ship-details.js new file mode 100644 index 0000000..5fe0435 --- /dev/null +++ b/exercises/04.router/05.solution.cache/ui/ship-details.js @@ -0,0 +1,113 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + initialValue: 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/04.router/05.solution.cache/ui/ship-search-results.js b/exercises/04.router/05.solution.cache/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/04.router/05.solution.cache/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/04.router/05.solution.cache/ui/ship-search.js b/exercises/04.router/05.solution.cache/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/04.router/05.solution.cache/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/04.router/05.solution.cache/ui/spin-delay.js b/exercises/04.router/05.solution.cache/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/04.router/05.solution.cache/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/04.router/FINISHED.mdx b/exercises/04.router/FINISHED.mdx new file mode 100644 index 0000000..c38b14f --- /dev/null +++ b/exercises/04.router/FINISHED.mdx @@ -0,0 +1,8 @@ +# Client Router + + + +๐Ÿ‘จโ€๐Ÿ’ผ React Server Components really rely on integration with your router to handle +transitions that affect even parts of the UI that were generated by the server +components. You've implemented a simple router with integrated RSC support, +great job! diff --git a/exercises/04.router/README.mdx b/exercises/04.router/README.mdx new file mode 100644 index 0000000..339908e --- /dev/null +++ b/exercises/04.router/README.mdx @@ -0,0 +1,30 @@ +# Client Router + + + +As the user navigates around your application, you need to keep the UI +up-to-date with what they should be looking at. Traditionally this means lazy +loading code for the new routes and requesting relevant data. + +This is no different for React Server Components. However, instead of fetching +code and data, we fetch the up-to-date RSC payload (which is kind of like code +and data combined) and update the UI with that updated payload. + +There are a lot of things to consider with this. For one, we don't want a full +page reload whenever users click links within the app as this is a poor user +experience and makes it harder to manage things like scroll position and focus. + +We also want to do better than the browser can with regard to pending UI (really +anything is better than just the favicon turning into a spinner). So the router +needs to expose a mechanism to determine the location you're transitioning to +so the existing UI can be updated with a loading state while the new UI is +generated. + +You'll want to handle race conditions in case the user gets a little indecisive +and clicks around a lot. + +Finally, you'll want to also handling forward/back buttons to update the UI +to what it should be. + +All of this is rather involved and can be a bit tricky to get right. We're going +to implement a simple version of a router in this exercise. diff --git a/exercises/05.actions/01.problem.action-reference/.gitignore b/exercises/05.actions/01.problem.action-reference/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/05.actions/01.problem.action-reference/README.mdx b/exercises/05.actions/01.problem.action-reference/README.mdx new file mode 100644 index 0000000..1e445ec --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/README.mdx @@ -0,0 +1,41 @@ +# Action Reference + + + +๐Ÿ‘จโ€๐Ÿ’ผ Our users want to be able to change the name of the space ships. So Kellie +has brought in a new client component for editing the name. All you need to do +is click on the name in the details view and you'll be able to edit it. + +Thanks to the work we did setting up the server for React Server Components, +our server is already set up to handle server actions. By adding a +`'use server'` to the top of the module, our Node.js loader we set up earlier +will handle converting our server actions into references which we'll send to +the client. + +In this step, we're going to wire the server action up to the client and you can +take a look at the RSC output to find the reference in there. + +๐Ÿจ First, let's update the file to respond +properly. Then you can go to to import +that action and send it along to the client component. + +๐Ÿจ Finally in you can accept the action +and wire things up with the `useActionState()` hook from `'react'`. + +Once you've done this, then you should be able to open the rsc endpoint for a +ship (like ) and you should be able to find +the action reference in the output (search for "actions.js"). + + + This step is only part of the work. When you're finished with this step, you + won't have anything working quite yet. Continue onto the next step. + + + + `react-server-dom-esm` does not allow you to specify a `method` or `encType` + on a `form` element when you specify an `action` that's a reference to a + server action. Instead, it will set those values at request time for you. I + personally don't agree with this decision (I would prefer to stick with the + platform defaults), but the React team feels like it's worth straying from the + platform defaults in this case. + diff --git a/exercises/05.actions/01.problem.action-reference/db/ship-api.js b/exercises/05.actions/01.problem.action-reference/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/05.actions/01.problem.action-reference/db/ships.json b/exercises/05.actions/01.problem.action-reference/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/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/05.actions/01.problem.action-reference/package.json b/exercises/05.actions/01.problem.action-reference/package.json new file mode 100644 index 0000000..67c0d83 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__05.actions__sep__01.problem.action-reference", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/05.actions/01.problem.action-reference/public/favicon.ico b/exercises/05.actions/01.problem.action-reference/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/favicon.ico differ diff --git a/exercises/05.actions/01.problem.action-reference/public/favicon.svg b/exercises/05.actions/01.problem.action-reference/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/05.actions/01.problem.action-reference/public/iframe-sync.js b/exercises/05.actions/01.problem.action-reference/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/05.actions/01.problem.action-reference/public/img/broken-ship.webp b/exercises/05.actions/01.problem.action-reference/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/broken-ship.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/fallback-ship.png b/exercises/05.actions/01.problem.action-reference/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/fallback-ship.png differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/0268fc4817ad1.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/1ae7b4b92036b.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/1ff1991efe029.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/3ba8aa65ffe6c.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/441f7092a8d44.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/5c13d8b28a14a.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/627c497212456.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/627c497212456.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/670003aed3795.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/670003aed3795.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/6c86fca8b9086.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/6f375578ead88.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/ab267a5984523.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/b442531ea32b2.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/bc4cbadf89bd3.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/cb03cc4e5717e.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/cfd10fcd2de6c.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/d3b8aa65ffe6c.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/d486d48b82b81.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/e92cefe4f6727.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/ec7a3f950f99f.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/f3d9a88e1c234.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/img/ships/fdc13cb488bf1.webp b/exercises/05.actions/01.problem.action-reference/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/05.actions/01.problem.action-reference/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/05.actions/01.problem.action-reference/public/index.html b/exercises/05.actions/01.problem.action-reference/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/05.actions/01.problem.action-reference/public/style.css b/exercises/05.actions/01.problem.action-reference/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/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/05.actions/01.problem.action-reference/server/app.js b/exercises/05.actions/01.problem.action-reference/server/app.js new file mode 100644 index 0000000..2400155 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/server/app.js @@ -0,0 +1,83 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +const moduleBasePath = new URL('../ui', import.meta.url).href + +async function renderApp(context) { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const root = h(App) + const payload = root + const { pipe } = renderToPipeableStream(payload, moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +} + +app.get('/rsc/:shipId?', async (context) => await renderApp(context)) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/10.problem.actions/server/async-storage.js b/exercises/05.actions/01.problem.action-reference/server/async-storage.js similarity index 100% rename from exercises/01.exercises/10.problem.actions/server/async-storage.js rename to exercises/05.actions/01.problem.action-reference/server/async-storage.js diff --git a/exercises/05.actions/01.problem.action-reference/server/register-rsc-loader.js b/exercises/05.actions/01.problem.action-reference/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/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/05.actions/01.problem.action-reference/server/rsc-loader.js b/exercises/05.actions/01.problem.action-reference/server/rsc-loader.js new file mode 100644 index 0000000..9679650 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/server/rsc-loader.js @@ -0,0 +1,28 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + // ๐Ÿ’ฐ uncomment this to see how our loader transforms the actions file + // if (url.includes('actions.js')) { + // console.log(result.source) + // } + return result +} diff --git a/exercises/05.actions/01.problem.action-reference/tests/action-reference.test.js b/exercises/05.actions/01.problem.action-reference/tests/action-reference.test.js new file mode 100644 index 0000000..ba974bd --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/tests/action-reference.test.js @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test' +import { searchShips } from '../db/ship-api.js' + +test('RSC endpoint response includes reference to actions.js', async ({ + page, +}) => { + const { + ships: [firstShip], + } = await searchShips({ search: '' }) + await page.goto(`/${firstShip.id}`) + + // Make a GET request to the /rsc endpoint + const response = await page.request.get(`/rsc/${firstShip.id}`) + + // Get the response text + const responseText = await response.text() + + // Verify that the response includes "actions.js" + expect(responseText).toContain('actions.js') +}) diff --git a/exercises/05.actions/01.problem.action-reference/tests/playwright.config.js b/exercises/05.actions/01.problem.action-reference/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/05.actions/01.problem.action-reference/tests/smoke.test.js b/exercises/05.actions/01.problem.action-reference/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/05.actions/01.problem.action-reference/ui/actions.js b/exercises/05.actions/01.problem.action-reference/ui/actions.js new file mode 100644 index 0000000..990a6d2 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/ui/actions.js @@ -0,0 +1,16 @@ +// ๐Ÿจ add 'use server' here to turn this module into an RSC reference + +import * as db from '../db/ship-api.js' + +export async function updateShipName(previousState, formData) { + try { + await db.updateShipName({ + shipId: formData.get('shipId'), + shipName: formData.get('shipName'), + }) + // ๐Ÿจ return a status of 'success' and a message of 'Success!' + } catch (error) { + console.error(error) + // ๐Ÿจ return a status of 'error' and a message of error?.message || String(error) + } +} diff --git a/exercises/05.actions/01.problem.action-reference/ui/app.js b/exercises/05.actions/01.problem.action-reference/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/05.actions/01.problem.action-reference/ui/content-cache.js b/exercises/05.actions/01.problem.action-reference/ui/content-cache.js new file mode 100644 index 0000000..612b97f --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/ui/content-cache.js @@ -0,0 +1,46 @@ +import { useSyncExternalStore } from 'react' + +/** + * This will call the given callback function whenever the contents of the map + * change. + */ +class ObservableMap extends Map { + listeners = new Set() + set(key, value) { + const result = super.set(key, value) + this.emitChange() + return result + } + delete(key) { + const result = super.delete(key) + this.emitChange() + return result + } + emitChange() { + for (const listener of this.listeners) { + listener() + } + } + subscribe(listener) { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } +} + +export const contentCache = new ObservableMap() + +export function generateKey() { + return Date.now().toString(36) + Math.random().toString(36).slice(2) +} + +export function useContentCache() { + function subscribe(cb) { + return contentCache.subscribe(cb) + } + function getSnapshot() { + return contentCache + } + return useSyncExternalStore(subscribe, getSnapshot) +} diff --git a/exercises/05.actions/01.problem.action-reference/ui/edit-text.js b/exercises/05.actions/01.problem.action-reference/ui/edit-text.js new file mode 100644 index 0000000..53f3ee7 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/ui/edit-text.js @@ -0,0 +1,107 @@ +'use client' + +// ๐Ÿจ bring in useActionState from 'react' here +import { createElement as h, useRef, useState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +// ๐Ÿจ accept an action prop +export function EditableText({ id, shipId, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + // ๐Ÿจ get formState, formAction, and isPending from useActionState from react + // with the action from props + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + // ๐Ÿจ set the style prop on this div to decrease the opacity when the form is submitting + // something like { opacity: isPending ? 0.6 : 1 } should work + null, + edit + ? h( + 'form', + { + // ๐Ÿจ add an action prop and set it to formAction + onSubmit: (event) => { + // ๐Ÿจ remove preventDefault here since the action handles this for you + event.preventDefault() + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + h( + 'div', + { position: 'relative' }, + // ๐Ÿจ if we have formState, then display the formState.message here in a div + // ๐Ÿ’ฏ make the color red if it's an error and green if it's not + // ๐Ÿ’ฐ here are some handy styles for you: + // style: { + // position: 'absolute', + // left: 0, + // right: 0, + // color: formState.status === 'error' ? 'red' : 'green', + // fontSize: '0.75rem', + // fontWeight: 'normal', + // }, + ), + ) +} diff --git a/exercises/05.actions/01.problem.action-reference/ui/error-boundary.js b/exercises/05.actions/01.problem.action-reference/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/05.actions/01.problem.action-reference/ui/img-utils.js b/exercises/05.actions/01.problem.action-reference/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/05.actions/01.problem.action-reference/ui/img.js b/exercises/05.actions/01.problem.action-reference/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/05.actions/01.problem.action-reference/ui/index.js b/exercises/05.actions/01.problem.action-reference/ui/index.js new file mode 100644 index 0000000..86d7077 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/ui/index.js @@ -0,0 +1,132 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useEffect, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { contentCache, useContentCache, generateKey } from './content-cache.js' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + }) +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +let initialContentKey = window.history.state?.key +if (!initialContentKey) { + initialContentKey = generateKey() + window.history.replaceState({ key: initialContentKey }, '') +} +contentCache.set(initialContentKey, initialContentPromise) + +function Root() { + const latestNav = useRef(null) + const contentCache = useContentCache() + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentKey, setContentKey] = useState(initialContentKey) + const [isPending, startTransition] = useTransition() + + const location = useDeferredValue(nextLocation) + const contentPromise = contentCache.get(contentKey) + + useEffect(() => { + function handlePopState() { + const nextLocation = getGlobalLocation() + setNextLocation(nextLocation) + const historyKey = window.history.state?.key ?? generateKey() + + if (!contentCache.has(historyKey)) { + const fetchPromise = fetchContent(nextLocation) + const nextContentPromise = createFromFetch(fetchPromise) + contentCache.set(historyKey, nextContentPromise) + } + + startTransition(() => setContentKey(historyKey)) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, [contentCache]) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + const newContentKey = generateKey() + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + window.history.replaceState({ key: newContentKey }, '', nextLocation) + } else { + window.history.pushState({ key: newContentKey }, '', nextLocation) + } + return response + }), + ) + + contentCache.set(newContentKey, nextContentPromise) + startTransition(() => setContentKey(newContentKey)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/05.actions/01.problem.action-reference/ui/router.js b/exercises/05.actions/01.problem.action-reference/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/05.actions/01.problem.action-reference/ui/ship-details-pending.js b/exercises/05.actions/01.problem.action-reference/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/05.actions/01.problem.action-reference/ui/ship-details.js b/exercises/05.actions/01.problem.action-reference/ui/ship-details.js new file mode 100644 index 0000000..81a6a56 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/ui/ship-details.js @@ -0,0 +1,133 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +// ๐Ÿจ bring in updateShipName from the './actions.js' file +import { EditableText } from './edit-text.js' +import { getImageUrlForShip } from './img-utils.js' +import { ShipImg } from './img.js' + +// ๐Ÿ’ฐ you can log what extra properties the updateShipName function gets because +// it's in a 'use server' file: +// const properties = {} +// for (const [key, descriptor] of Object.entries( +// Object.getOwnPropertyDescriptors(updateShipName), +// )) { +// properties[key] = descriptor.value +// } + +// console.log(updateShipName.toString()) +// console.log( +// JSON.stringify( +// properties, +// (key, value) => (typeof value === 'object' ? value : String(value)), +// 2, +// ), +// ) + +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, + h(EditableText, { + key: shipId, + shipId, + // ๐Ÿจ add an action prop set to updateShipName + initialValue: 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/05.actions/01.problem.action-reference/ui/ship-search-results.js b/exercises/05.actions/01.problem.action-reference/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/05.actions/01.problem.action-reference/ui/ship-search.js b/exercises/05.actions/01.problem.action-reference/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/05.actions/01.problem.action-reference/ui/spin-delay.js b/exercises/05.actions/01.problem.action-reference/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/05.actions/01.problem.action-reference/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/05.actions/01.solution.action-reference/.gitignore b/exercises/05.actions/01.solution.action-reference/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/05.actions/01.solution.action-reference/README.mdx b/exercises/05.actions/01.solution.action-reference/README.mdx new file mode 100644 index 0000000..93db1f5 --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/README.mdx @@ -0,0 +1,52 @@ +# Action Reference + + + +๐Ÿ‘จโ€๐Ÿ’ผ Great! So this is what our Node.js loader does to the `actions.js` module: + +```js +'use server' + +import * as db from '../db/ship-api.js' + +export async function updateShipName(previousState, formData) { + try { + await db.updateShipName({ + shipId: formData.get('shipId'), + shipName: formData.get('shipName'), + }) + return { status: 'success', message: 'Success!' } + } catch (error) { + return { status: 'error', message: error?.message || String(error) } + } +} + +import { registerServerReference } from 'react-server-dom-esm/server' +registerServerReference( + updateShipName, + 'file:///Users/kentcdodds/code/epicweb-dev/react-server-components/playground/ui/actions.js', + 'updateShipName', +) +``` + +The `registerServerReference` function attaches this additional information onto +our `updateShipName` function: + +```json +{ + "$$typeof": "Symbol(react.server.reference)", + "$$id": "file:///Users/kentcdodds/code/epicweb-dev/react-server-components/playground/ui/actions.js#updateShipName", + "$$bound": null, + "bind": "function bind() {\n // $FlowFixMe[unsupported-syntax]\n var newFn = FunctionBind.apply(this, arguments);\n\n if (this.$$typeof === SERVER_REFERENCE_TAG) {\n // $FlowFixMe[method-unbinding]\n var args = ArraySlice.call(arguments, 1);\n return Object.defineProperties(newFn, {\n $$typeof: {\n value: SERVER_REFERENCE_TAG\n },\n $$id: {\n value: this.$$id\n },\n $$bound: {\n value: this.$$bound ? this.$$bound.concat(args) : args\n },\n bind: {\n value: bind\n }\n });\n }\n\n return newFn;\n}" +} +``` + +The serialized version of this function looks like this in our RSC payload: + +``` +d:{"id":"file:///Users/kentcdodds/code/epicweb-dev/react-server-components/playground/ui/actions.js#updateShipName","bound":null} +``` + +So it's the path to the module + `#` + the name of the function. This is how we +can find it on the server when the user submits the form. Let's get that passed +along to the server next! diff --git a/exercises/05.actions/01.solution.action-reference/db/ship-api.js b/exercises/05.actions/01.solution.action-reference/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/05.actions/01.solution.action-reference/db/ships.json b/exercises/05.actions/01.solution.action-reference/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/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/05.actions/01.solution.action-reference/package.json b/exercises/05.actions/01.solution.action-reference/package.json new file mode 100644 index 0000000..f91ccf4 --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__05.actions__sep__01.solution.action-reference", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/05.actions/01.solution.action-reference/public/favicon.ico b/exercises/05.actions/01.solution.action-reference/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/favicon.ico differ diff --git a/exercises/05.actions/01.solution.action-reference/public/favicon.svg b/exercises/05.actions/01.solution.action-reference/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/05.actions/01.solution.action-reference/public/iframe-sync.js b/exercises/05.actions/01.solution.action-reference/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/05.actions/01.solution.action-reference/public/img/broken-ship.webp b/exercises/05.actions/01.solution.action-reference/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/broken-ship.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/fallback-ship.png b/exercises/05.actions/01.solution.action-reference/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/fallback-ship.png differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/0268fc4817ad1.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/1ae7b4b92036b.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/1ff1991efe029.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/3ba8aa65ffe6c.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/441f7092a8d44.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/5c13d8b28a14a.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/627c497212456.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/627c497212456.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/670003aed3795.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/670003aed3795.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/6c86fca8b9086.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/6f375578ead88.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/ab267a5984523.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/b442531ea32b2.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/bc4cbadf89bd3.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/cb03cc4e5717e.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/cfd10fcd2de6c.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/d3b8aa65ffe6c.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/d486d48b82b81.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/e92cefe4f6727.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/ec7a3f950f99f.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/f3d9a88e1c234.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/img/ships/fdc13cb488bf1.webp b/exercises/05.actions/01.solution.action-reference/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/05.actions/01.solution.action-reference/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/05.actions/01.solution.action-reference/public/index.html b/exercises/05.actions/01.solution.action-reference/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/05.actions/01.solution.action-reference/public/style.css b/exercises/05.actions/01.solution.action-reference/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/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/05.actions/01.solution.action-reference/server/app.js b/exercises/05.actions/01.solution.action-reference/server/app.js new file mode 100644 index 0000000..2400155 --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/server/app.js @@ -0,0 +1,83 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +const moduleBasePath = new URL('../ui', import.meta.url).href + +async function renderApp(context) { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const root = h(App) + const payload = root + const { pipe } = renderToPipeableStream(payload, moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +} + +app.get('/rsc/:shipId?', async (context) => await renderApp(context)) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/01.exercises/10.solution.actions/server/async-storage.js b/exercises/05.actions/01.solution.action-reference/server/async-storage.js similarity index 100% rename from exercises/01.exercises/10.solution.actions/server/async-storage.js rename to exercises/05.actions/01.solution.action-reference/server/async-storage.js diff --git a/exercises/05.actions/01.solution.action-reference/server/register-rsc-loader.js b/exercises/05.actions/01.solution.action-reference/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/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/05.actions/01.solution.action-reference/server/rsc-loader.js b/exercises/05.actions/01.solution.action-reference/server/rsc-loader.js new file mode 100644 index 0000000..5ee1a1a --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/server/rsc-loader.js @@ -0,0 +1,27 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + if (url.includes('actions.js')) { + console.log(result.source) + } + return result +} diff --git a/exercises/05.actions/01.solution.action-reference/tests/action-reference.test.js b/exercises/05.actions/01.solution.action-reference/tests/action-reference.test.js new file mode 100644 index 0000000..ba974bd --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/tests/action-reference.test.js @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test' +import { searchShips } from '../db/ship-api.js' + +test('RSC endpoint response includes reference to actions.js', async ({ + page, +}) => { + const { + ships: [firstShip], + } = await searchShips({ search: '' }) + await page.goto(`/${firstShip.id}`) + + // Make a GET request to the /rsc endpoint + const response = await page.request.get(`/rsc/${firstShip.id}`) + + // Get the response text + const responseText = await response.text() + + // Verify that the response includes "actions.js" + expect(responseText).toContain('actions.js') +}) diff --git a/exercises/05.actions/01.solution.action-reference/tests/playwright.config.js b/exercises/05.actions/01.solution.action-reference/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/05.actions/01.solution.action-reference/tests/smoke.test.js b/exercises/05.actions/01.solution.action-reference/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/05.actions/01.solution.action-reference/ui/actions.js b/exercises/05.actions/01.solution.action-reference/ui/actions.js new file mode 100644 index 0000000..71008e9 --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/ui/actions.js @@ -0,0 +1,15 @@ +'use server' + +import * as db from '../db/ship-api.js' + +export async function updateShipName(previousState, formData) { + try { + await db.updateShipName({ + shipId: formData.get('shipId'), + shipName: formData.get('shipName'), + }) + return { status: 'success', message: 'Success!' } + } catch (error) { + return { status: 'error', message: error?.message || String(error) } + } +} diff --git a/exercises/05.actions/01.solution.action-reference/ui/app.js b/exercises/05.actions/01.solution.action-reference/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/05.actions/01.solution.action-reference/ui/content-cache.js b/exercises/05.actions/01.solution.action-reference/ui/content-cache.js new file mode 100644 index 0000000..612b97f --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/ui/content-cache.js @@ -0,0 +1,46 @@ +import { useSyncExternalStore } from 'react' + +/** + * This will call the given callback function whenever the contents of the map + * change. + */ +class ObservableMap extends Map { + listeners = new Set() + set(key, value) { + const result = super.set(key, value) + this.emitChange() + return result + } + delete(key) { + const result = super.delete(key) + this.emitChange() + return result + } + emitChange() { + for (const listener of this.listeners) { + listener() + } + } + subscribe(listener) { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } +} + +export const contentCache = new ObservableMap() + +export function generateKey() { + return Date.now().toString(36) + Math.random().toString(36).slice(2) +} + +export function useContentCache() { + function subscribe(cb) { + return contentCache.subscribe(cb) + } + function getSnapshot() { + return contentCache + } + return useSyncExternalStore(subscribe, getSnapshot) +} diff --git a/exercises/05.actions/01.solution.action-reference/ui/edit-text.js b/exercises/05.actions/01.solution.action-reference/ui/edit-text.js new file mode 100644 index 0000000..1904aad --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/ui/edit-text.js @@ -0,0 +1,105 @@ +'use client' + +import { createElement as h, useRef, useState, useActionState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, action, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const [formState, formAction, isPending] = useActionState(action) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + { style: { opacity: isPending ? 0.6 : 1 } }, + edit + ? h( + 'form', + { + action: formAction, + onSubmit: () => { + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + h( + 'div', + { position: 'relative' }, + formState + ? h( + 'div', + { + style: { + position: 'absolute', + left: 0, + right: 0, + color: formState.status === 'error' ? 'red' : 'green', + fontSize: '0.75rem', + fontWeight: 'normal', + }, + }, + formState.message, + ) + : null, + ), + ) +} diff --git a/exercises/05.actions/01.solution.action-reference/ui/error-boundary.js b/exercises/05.actions/01.solution.action-reference/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/05.actions/01.solution.action-reference/ui/img-utils.js b/exercises/05.actions/01.solution.action-reference/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/05.actions/01.solution.action-reference/ui/img.js b/exercises/05.actions/01.solution.action-reference/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/05.actions/01.solution.action-reference/ui/index.js b/exercises/05.actions/01.solution.action-reference/ui/index.js new file mode 100644 index 0000000..86d7077 --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/ui/index.js @@ -0,0 +1,132 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useEffect, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { contentCache, useContentCache, generateKey } from './content-cache.js' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + }) +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +let initialContentKey = window.history.state?.key +if (!initialContentKey) { + initialContentKey = generateKey() + window.history.replaceState({ key: initialContentKey }, '') +} +contentCache.set(initialContentKey, initialContentPromise) + +function Root() { + const latestNav = useRef(null) + const contentCache = useContentCache() + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentKey, setContentKey] = useState(initialContentKey) + const [isPending, startTransition] = useTransition() + + const location = useDeferredValue(nextLocation) + const contentPromise = contentCache.get(contentKey) + + useEffect(() => { + function handlePopState() { + const nextLocation = getGlobalLocation() + setNextLocation(nextLocation) + const historyKey = window.history.state?.key ?? generateKey() + + if (!contentCache.has(historyKey)) { + const fetchPromise = fetchContent(nextLocation) + const nextContentPromise = createFromFetch(fetchPromise) + contentCache.set(historyKey, nextContentPromise) + } + + startTransition(() => setContentKey(historyKey)) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, [contentCache]) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + const newContentKey = generateKey() + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + window.history.replaceState({ key: newContentKey }, '', nextLocation) + } else { + window.history.pushState({ key: newContentKey }, '', nextLocation) + } + return response + }), + ) + + contentCache.set(newContentKey, nextContentPromise) + startTransition(() => setContentKey(newContentKey)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/05.actions/01.solution.action-reference/ui/router.js b/exercises/05.actions/01.solution.action-reference/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/05.actions/01.solution.action-reference/ui/ship-details-pending.js b/exercises/05.actions/01.solution.action-reference/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/05.actions/01.solution.action-reference/ui/ship-details.js b/exercises/05.actions/01.solution.action-reference/ui/ship-details.js new file mode 100644 index 0000000..088b9b4 --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/ui/ship-details.js @@ -0,0 +1,131 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { updateShipName } from './actions.js' +import { EditableText } from './edit-text.js' +import { getImageUrlForShip } from './img-utils.js' +import { ShipImg } from './img.js' + +const properties = {} +for (const [key, descriptor] of Object.entries( + Object.getOwnPropertyDescriptors(updateShipName), +)) { + properties[key] = descriptor.value +} + +console.log(updateShipName.toString()) +console.log( + JSON.stringify( + properties, + (key, value) => (typeof value === 'object' ? value : String(value)), + 2, + ), +) + +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, + h(EditableText, { + key: shipId, + shipId, + action: updateShipName, + initialValue: 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/05.actions/01.solution.action-reference/ui/ship-search-results.js b/exercises/05.actions/01.solution.action-reference/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/05.actions/01.solution.action-reference/ui/ship-search.js b/exercises/05.actions/01.solution.action-reference/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/05.actions/01.solution.action-reference/ui/spin-delay.js b/exercises/05.actions/01.solution.action-reference/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/05.actions/01.solution.action-reference/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/05.actions/02.problem.client/.gitignore b/exercises/05.actions/02.problem.client/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/05.actions/02.problem.client/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/05.actions/02.problem.client/README.mdx b/exercises/05.actions/02.problem.client/README.mdx new file mode 100644 index 0000000..1e854b0 --- /dev/null +++ b/exercises/05.actions/02.problem.client/README.mdx @@ -0,0 +1,21 @@ +# Client Side + + + +When we fetch an RSC payload, it's going to have our action in there. +`react-server-dom-esm/client` needs to be told what to do when the action is +called. + +This is where the `callServer` function comes in. You pass that as an option +to `createFromFetch` and whenever an action is called, it will call your +function. It's your job to send the action to the server and return the +return value. + +So please implement the `callServer` function +in and make sure it sends the action to the +server. + + + At this stage, our server doesn't handle the POST request yet, but you can + check the Network devtools to see that the request is being sent properly. + diff --git a/exercises/05.actions/02.problem.client/db/ship-api.js b/exercises/05.actions/02.problem.client/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/05.actions/02.problem.client/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/05.actions/02.problem.client/db/ships.json b/exercises/05.actions/02.problem.client/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/05.actions/02.problem.client/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/05.actions/02.problem.client/package.json b/exercises/05.actions/02.problem.client/package.json new file mode 100644 index 0000000..2dd7d2b --- /dev/null +++ b/exercises/05.actions/02.problem.client/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__05.actions__sep__02.problem.client", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/05.actions/02.problem.client/public/favicon.ico b/exercises/05.actions/02.problem.client/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/favicon.ico differ diff --git a/exercises/05.actions/02.problem.client/public/favicon.svg b/exercises/05.actions/02.problem.client/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/05.actions/02.problem.client/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/05.actions/02.problem.client/public/iframe-sync.js b/exercises/05.actions/02.problem.client/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/05.actions/02.problem.client/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/05.actions/02.problem.client/public/img/broken-ship.webp b/exercises/05.actions/02.problem.client/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/broken-ship.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/fallback-ship.png b/exercises/05.actions/02.problem.client/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/fallback-ship.png differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/0268fc4817ad1.webp b/exercises/05.actions/02.problem.client/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/1ae7b4b92036b.webp b/exercises/05.actions/02.problem.client/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/1ff1991efe029.webp b/exercises/05.actions/02.problem.client/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/3ba8aa65ffe6c.webp b/exercises/05.actions/02.problem.client/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/441f7092a8d44.webp b/exercises/05.actions/02.problem.client/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/5c13d8b28a14a.webp b/exercises/05.actions/02.problem.client/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/627c497212456.webp b/exercises/05.actions/02.problem.client/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/627c497212456.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/670003aed3795.webp b/exercises/05.actions/02.problem.client/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/670003aed3795.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/6c86fca8b9086.webp b/exercises/05.actions/02.problem.client/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/6f375578ead88.webp b/exercises/05.actions/02.problem.client/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/ab267a5984523.webp b/exercises/05.actions/02.problem.client/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/b442531ea32b2.webp b/exercises/05.actions/02.problem.client/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/bc4cbadf89bd3.webp b/exercises/05.actions/02.problem.client/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/cb03cc4e5717e.webp b/exercises/05.actions/02.problem.client/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/cfd10fcd2de6c.webp b/exercises/05.actions/02.problem.client/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/d3b8aa65ffe6c.webp b/exercises/05.actions/02.problem.client/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/d486d48b82b81.webp b/exercises/05.actions/02.problem.client/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/e92cefe4f6727.webp b/exercises/05.actions/02.problem.client/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/ec7a3f950f99f.webp b/exercises/05.actions/02.problem.client/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/f3d9a88e1c234.webp b/exercises/05.actions/02.problem.client/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/05.actions/02.problem.client/public/img/ships/fdc13cb488bf1.webp b/exercises/05.actions/02.problem.client/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/05.actions/02.problem.client/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/05.actions/02.problem.client/public/index.html b/exercises/05.actions/02.problem.client/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/05.actions/02.problem.client/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/05.actions/02.problem.client/public/style.css b/exercises/05.actions/02.problem.client/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/05.actions/02.problem.client/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/05.actions/02.problem.client/server/app.js b/exercises/05.actions/02.problem.client/server/app.js new file mode 100644 index 0000000..2400155 --- /dev/null +++ b/exercises/05.actions/02.problem.client/server/app.js @@ -0,0 +1,83 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +const moduleBasePath = new URL('../ui', import.meta.url).href + +async function renderApp(context) { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const root = h(App) + const payload = root + const { pipe } = renderToPipeableStream(payload, moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +} + +app.get('/rsc/:shipId?', async (context) => await renderApp(context)) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/05.actions/02.problem.client/server/async-storage.js b/exercises/05.actions/02.problem.client/server/async-storage.js new file mode 100644 index 0000000..b9a08b1 --- /dev/null +++ b/exercises/05.actions/02.problem.client/server/async-storage.js @@ -0,0 +1,3 @@ +import { AsyncLocalStorage } from 'node:async_hooks' + +export const shipDataStorage = new AsyncLocalStorage() diff --git a/exercises/05.actions/02.problem.client/server/register-rsc-loader.js b/exercises/05.actions/02.problem.client/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/05.actions/02.problem.client/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/05.actions/02.problem.client/server/rsc-loader.js b/exercises/05.actions/02.problem.client/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/05.actions/02.problem.client/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/05.actions/02.problem.client/tests/action-post.test.js b/exercises/05.actions/02.problem.client/tests/action-post.test.js new file mode 100644 index 0000000..d30809a --- /dev/null +++ b/exercises/05.actions/02.problem.client/tests/action-post.test.js @@ -0,0 +1,46 @@ +import { test, expect } from '@playwright/test' +import { searchShips } from '../db/ship-api.js' + +test('Submitting the form posts to the action endpoint correctly', async ({ + page, +}) => { + const { + ships: [ship], + } = await searchShips({ search: '' }) + await page.goto(`/${ship.id}`) + await page.waitForLoadState('networkidle') + + await page.getByRole('button', { name: ship.name }).click() + + const newName = `${ship.name} ${Math.random().toString(16).slice(2, 5)}` + + // Change the value of the input + await page.getByRole('textbox', { name: 'Ship Name' }).fill(newName) + + // Intercept the request to /action + const actionRequest = page.waitForRequest((request) => { + return request.url().includes('/action') && request.method() === 'POST' + }) + + // Press Enter + await page.keyboard.press('Enter') + + // Wait for the request to be made + const request = await actionRequest + + // Verify the request URL + expect(request.url()).toContain(`/action/${ship.id}`) + + // Verify the request status (should be 404 as per instructions) + const response = await request.response() + expect(response.status()).toBe(404) + + // Verify the form data payload + const postData = await request.postData() + expect(postData).toContain(newName) + expect(postData).toContain(ship.id) + + // Verify the rsc-action header + const headers = request.headers() + expect(headers['rsc-action']).toContain('ui/actions.js#updateShipName') +}) diff --git a/exercises/05.actions/02.problem.client/tests/playwright.config.js b/exercises/05.actions/02.problem.client/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/05.actions/02.problem.client/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/05.actions/02.problem.client/tests/smoke.test.js b/exercises/05.actions/02.problem.client/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/05.actions/02.problem.client/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/05.actions/02.problem.client/ui/actions.js b/exercises/05.actions/02.problem.client/ui/actions.js new file mode 100644 index 0000000..71008e9 --- /dev/null +++ b/exercises/05.actions/02.problem.client/ui/actions.js @@ -0,0 +1,15 @@ +'use server' + +import * as db from '../db/ship-api.js' + +export async function updateShipName(previousState, formData) { + try { + await db.updateShipName({ + shipId: formData.get('shipId'), + shipName: formData.get('shipName'), + }) + return { status: 'success', message: 'Success!' } + } catch (error) { + return { status: 'error', message: error?.message || String(error) } + } +} diff --git a/exercises/05.actions/02.problem.client/ui/app.js b/exercises/05.actions/02.problem.client/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/05.actions/02.problem.client/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/05.actions/02.problem.client/ui/content-cache.js b/exercises/05.actions/02.problem.client/ui/content-cache.js new file mode 100644 index 0000000..612b97f --- /dev/null +++ b/exercises/05.actions/02.problem.client/ui/content-cache.js @@ -0,0 +1,46 @@ +import { useSyncExternalStore } from 'react' + +/** + * This will call the given callback function whenever the contents of the map + * change. + */ +class ObservableMap extends Map { + listeners = new Set() + set(key, value) { + const result = super.set(key, value) + this.emitChange() + return result + } + delete(key) { + const result = super.delete(key) + this.emitChange() + return result + } + emitChange() { + for (const listener of this.listeners) { + listener() + } + } + subscribe(listener) { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } +} + +export const contentCache = new ObservableMap() + +export function generateKey() { + return Date.now().toString(36) + Math.random().toString(36).slice(2) +} + +export function useContentCache() { + function subscribe(cb) { + return contentCache.subscribe(cb) + } + function getSnapshot() { + return contentCache + } + return useSyncExternalStore(subscribe, getSnapshot) +} diff --git a/exercises/05.actions/02.problem.client/ui/edit-text.js b/exercises/05.actions/02.problem.client/ui/edit-text.js new file mode 100644 index 0000000..1904aad --- /dev/null +++ b/exercises/05.actions/02.problem.client/ui/edit-text.js @@ -0,0 +1,105 @@ +'use client' + +import { createElement as h, useRef, useState, useActionState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, action, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const [formState, formAction, isPending] = useActionState(action) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + { style: { opacity: isPending ? 0.6 : 1 } }, + edit + ? h( + 'form', + { + action: formAction, + onSubmit: () => { + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + h( + 'div', + { position: 'relative' }, + formState + ? h( + 'div', + { + style: { + position: 'absolute', + left: 0, + right: 0, + color: formState.status === 'error' ? 'red' : 'green', + fontSize: '0.75rem', + fontWeight: 'normal', + }, + }, + formState.message, + ) + : null, + ), + ) +} diff --git a/exercises/05.actions/02.problem.client/ui/error-boundary.js b/exercises/05.actions/02.problem.client/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/05.actions/02.problem.client/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/05.actions/02.problem.client/ui/img-utils.js b/exercises/05.actions/02.problem.client/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/05.actions/02.problem.client/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/05.actions/02.problem.client/ui/img.js b/exercises/05.actions/02.problem.client/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/05.actions/02.problem.client/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/05.actions/02.problem.client/ui/index.js b/exercises/05.actions/02.problem.client/ui/index.js new file mode 100644 index 0000000..db6e073 --- /dev/null +++ b/exercises/05.actions/02.problem.client/ui/index.js @@ -0,0 +1,143 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useEffect, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { contentCache, useContentCache, generateKey } from './content-cache.js' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + // ๐Ÿจ pass callServer here + }) +} + +// ๐Ÿจ create an async function called callServer +// ๐Ÿจ it should accept an id and args +// ๐Ÿจ it should fetch(`/action/${getGlobalLocation()}`) +// - the fetch method should be 'POST' +// - the headers should include {'rsc-action': id} +// - the body should be the encoded args via await RSC.encodeReply(args) +// ๐Ÿจ then create a new actionResponsePromise using createFromFetch +// ๐Ÿจ await the actionResponsePromise and destructure a property called "returnValue" +// ๐Ÿจ return the returnValue + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +let initialContentKey = window.history.state?.key +if (!initialContentKey) { + initialContentKey = generateKey() + window.history.replaceState({ key: initialContentKey }, '') +} +contentCache.set(initialContentKey, initialContentPromise) + +function Root() { + const latestNav = useRef(null) + const contentCache = useContentCache() + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentKey, setContentKey] = useState(initialContentKey) + const [isPending, startTransition] = useTransition() + + const location = useDeferredValue(nextLocation) + const contentPromise = contentCache.get(contentKey) + + useEffect(() => { + function handlePopState() { + const nextLocation = getGlobalLocation() + setNextLocation(nextLocation) + const historyKey = window.history.state?.key ?? generateKey() + + if (!contentCache.has(historyKey)) { + const fetchPromise = fetchContent(nextLocation) + const nextContentPromise = createFromFetch(fetchPromise) + contentCache.set(historyKey, nextContentPromise) + } + + startTransition(() => setContentKey(historyKey)) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, [contentCache]) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + const newContentKey = generateKey() + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + window.history.replaceState({ key: newContentKey }, '', nextLocation) + } else { + window.history.pushState({ key: newContentKey }, '', nextLocation) + } + return response + }), + ) + + contentCache.set(newContentKey, nextContentPromise) + startTransition(() => setContentKey(newContentKey)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/05.actions/02.problem.client/ui/router.js b/exercises/05.actions/02.problem.client/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/05.actions/02.problem.client/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/05.actions/02.problem.client/ui/ship-details-pending.js b/exercises/05.actions/02.problem.client/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/05.actions/02.problem.client/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/05.actions/02.problem.client/ui/ship-details.js b/exercises/05.actions/02.problem.client/ui/ship-details.js new file mode 100644 index 0000000..cd702fa --- /dev/null +++ b/exercises/05.actions/02.problem.client/ui/ship-details.js @@ -0,0 +1,115 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { updateShipName } from './actions.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + action: updateShipName, + initialValue: 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/05.actions/02.problem.client/ui/ship-search-results.js b/exercises/05.actions/02.problem.client/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/05.actions/02.problem.client/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/05.actions/02.problem.client/ui/ship-search.js b/exercises/05.actions/02.problem.client/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/05.actions/02.problem.client/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/05.actions/02.problem.client/ui/spin-delay.js b/exercises/05.actions/02.problem.client/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/05.actions/02.problem.client/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/05.actions/02.solution.client/.gitignore b/exercises/05.actions/02.solution.client/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/05.actions/02.solution.client/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/05.actions/02.solution.client/README.mdx b/exercises/05.actions/02.solution.client/README.mdx new file mode 100644 index 0000000..f303560 --- /dev/null +++ b/exercises/05.actions/02.solution.client/README.mdx @@ -0,0 +1,5 @@ +# Client Side + + + +๐Ÿ‘จโ€๐Ÿ’ผ You've done well. Let's finish this up on the server! diff --git a/exercises/05.actions/02.solution.client/db/ship-api.js b/exercises/05.actions/02.solution.client/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/05.actions/02.solution.client/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/05.actions/02.solution.client/db/ships.json b/exercises/05.actions/02.solution.client/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/05.actions/02.solution.client/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/05.actions/02.solution.client/package.json b/exercises/05.actions/02.solution.client/package.json new file mode 100644 index 0000000..7b9b0ed --- /dev/null +++ b/exercises/05.actions/02.solution.client/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__05.actions__sep__02.solution.client", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/05.actions/02.solution.client/public/favicon.ico b/exercises/05.actions/02.solution.client/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/favicon.ico differ diff --git a/exercises/05.actions/02.solution.client/public/favicon.svg b/exercises/05.actions/02.solution.client/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/05.actions/02.solution.client/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/05.actions/02.solution.client/public/iframe-sync.js b/exercises/05.actions/02.solution.client/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/05.actions/02.solution.client/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/05.actions/02.solution.client/public/img/broken-ship.webp b/exercises/05.actions/02.solution.client/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/broken-ship.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/fallback-ship.png b/exercises/05.actions/02.solution.client/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/fallback-ship.png differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/0268fc4817ad1.webp b/exercises/05.actions/02.solution.client/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/1ae7b4b92036b.webp b/exercises/05.actions/02.solution.client/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/1ff1991efe029.webp b/exercises/05.actions/02.solution.client/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/3ba8aa65ffe6c.webp b/exercises/05.actions/02.solution.client/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/441f7092a8d44.webp b/exercises/05.actions/02.solution.client/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/5c13d8b28a14a.webp b/exercises/05.actions/02.solution.client/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/627c497212456.webp b/exercises/05.actions/02.solution.client/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/627c497212456.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/670003aed3795.webp b/exercises/05.actions/02.solution.client/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/670003aed3795.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/6c86fca8b9086.webp b/exercises/05.actions/02.solution.client/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/6f375578ead88.webp b/exercises/05.actions/02.solution.client/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/ab267a5984523.webp b/exercises/05.actions/02.solution.client/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/b442531ea32b2.webp b/exercises/05.actions/02.solution.client/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/bc4cbadf89bd3.webp b/exercises/05.actions/02.solution.client/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/cb03cc4e5717e.webp b/exercises/05.actions/02.solution.client/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/cfd10fcd2de6c.webp b/exercises/05.actions/02.solution.client/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/d3b8aa65ffe6c.webp b/exercises/05.actions/02.solution.client/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/d486d48b82b81.webp b/exercises/05.actions/02.solution.client/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/e92cefe4f6727.webp b/exercises/05.actions/02.solution.client/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/ec7a3f950f99f.webp b/exercises/05.actions/02.solution.client/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/f3d9a88e1c234.webp b/exercises/05.actions/02.solution.client/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/05.actions/02.solution.client/public/img/ships/fdc13cb488bf1.webp b/exercises/05.actions/02.solution.client/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/05.actions/02.solution.client/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/05.actions/02.solution.client/public/index.html b/exercises/05.actions/02.solution.client/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/05.actions/02.solution.client/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/05.actions/02.solution.client/public/style.css b/exercises/05.actions/02.solution.client/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/05.actions/02.solution.client/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/05.actions/02.solution.client/server/app.js b/exercises/05.actions/02.solution.client/server/app.js new file mode 100644 index 0000000..2400155 --- /dev/null +++ b/exercises/05.actions/02.solution.client/server/app.js @@ -0,0 +1,83 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { renderToPipeableStream } from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +const moduleBasePath = new URL('../ui', import.meta.url).href + +async function renderApp(context) { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const root = h(App) + const payload = root + const { pipe } = renderToPipeableStream(payload, moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +} + +app.get('/rsc/:shipId?', async (context) => await renderApp(context)) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/05.actions/02.solution.client/server/async-storage.js b/exercises/05.actions/02.solution.client/server/async-storage.js new file mode 100644 index 0000000..b9a08b1 --- /dev/null +++ b/exercises/05.actions/02.solution.client/server/async-storage.js @@ -0,0 +1,3 @@ +import { AsyncLocalStorage } from 'node:async_hooks' + +export const shipDataStorage = new AsyncLocalStorage() diff --git a/exercises/05.actions/02.solution.client/server/register-rsc-loader.js b/exercises/05.actions/02.solution.client/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/05.actions/02.solution.client/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/05.actions/02.solution.client/server/rsc-loader.js b/exercises/05.actions/02.solution.client/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/05.actions/02.solution.client/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/05.actions/02.solution.client/tests/action-post.test.js b/exercises/05.actions/02.solution.client/tests/action-post.test.js new file mode 100644 index 0000000..d30809a --- /dev/null +++ b/exercises/05.actions/02.solution.client/tests/action-post.test.js @@ -0,0 +1,46 @@ +import { test, expect } from '@playwright/test' +import { searchShips } from '../db/ship-api.js' + +test('Submitting the form posts to the action endpoint correctly', async ({ + page, +}) => { + const { + ships: [ship], + } = await searchShips({ search: '' }) + await page.goto(`/${ship.id}`) + await page.waitForLoadState('networkidle') + + await page.getByRole('button', { name: ship.name }).click() + + const newName = `${ship.name} ${Math.random().toString(16).slice(2, 5)}` + + // Change the value of the input + await page.getByRole('textbox', { name: 'Ship Name' }).fill(newName) + + // Intercept the request to /action + const actionRequest = page.waitForRequest((request) => { + return request.url().includes('/action') && request.method() === 'POST' + }) + + // Press Enter + await page.keyboard.press('Enter') + + // Wait for the request to be made + const request = await actionRequest + + // Verify the request URL + expect(request.url()).toContain(`/action/${ship.id}`) + + // Verify the request status (should be 404 as per instructions) + const response = await request.response() + expect(response.status()).toBe(404) + + // Verify the form data payload + const postData = await request.postData() + expect(postData).toContain(newName) + expect(postData).toContain(ship.id) + + // Verify the rsc-action header + const headers = request.headers() + expect(headers['rsc-action']).toContain('ui/actions.js#updateShipName') +}) diff --git a/exercises/05.actions/02.solution.client/tests/playwright.config.js b/exercises/05.actions/02.solution.client/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/05.actions/02.solution.client/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/05.actions/02.solution.client/tests/smoke.test.js b/exercises/05.actions/02.solution.client/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/05.actions/02.solution.client/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/05.actions/02.solution.client/ui/actions.js b/exercises/05.actions/02.solution.client/ui/actions.js new file mode 100644 index 0000000..71008e9 --- /dev/null +++ b/exercises/05.actions/02.solution.client/ui/actions.js @@ -0,0 +1,15 @@ +'use server' + +import * as db from '../db/ship-api.js' + +export async function updateShipName(previousState, formData) { + try { + await db.updateShipName({ + shipId: formData.get('shipId'), + shipName: formData.get('shipName'), + }) + return { status: 'success', message: 'Success!' } + } catch (error) { + return { status: 'error', message: error?.message || String(error) } + } +} diff --git a/exercises/05.actions/02.solution.client/ui/app.js b/exercises/05.actions/02.solution.client/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/05.actions/02.solution.client/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/05.actions/02.solution.client/ui/content-cache.js b/exercises/05.actions/02.solution.client/ui/content-cache.js new file mode 100644 index 0000000..612b97f --- /dev/null +++ b/exercises/05.actions/02.solution.client/ui/content-cache.js @@ -0,0 +1,46 @@ +import { useSyncExternalStore } from 'react' + +/** + * This will call the given callback function whenever the contents of the map + * change. + */ +class ObservableMap extends Map { + listeners = new Set() + set(key, value) { + const result = super.set(key, value) + this.emitChange() + return result + } + delete(key) { + const result = super.delete(key) + this.emitChange() + return result + } + emitChange() { + for (const listener of this.listeners) { + listener() + } + } + subscribe(listener) { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } +} + +export const contentCache = new ObservableMap() + +export function generateKey() { + return Date.now().toString(36) + Math.random().toString(36).slice(2) +} + +export function useContentCache() { + function subscribe(cb) { + return contentCache.subscribe(cb) + } + function getSnapshot() { + return contentCache + } + return useSyncExternalStore(subscribe, getSnapshot) +} diff --git a/exercises/05.actions/02.solution.client/ui/edit-text.js b/exercises/05.actions/02.solution.client/ui/edit-text.js new file mode 100644 index 0000000..1904aad --- /dev/null +++ b/exercises/05.actions/02.solution.client/ui/edit-text.js @@ -0,0 +1,105 @@ +'use client' + +import { createElement as h, useRef, useState, useActionState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, action, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const [formState, formAction, isPending] = useActionState(action) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + { style: { opacity: isPending ? 0.6 : 1 } }, + edit + ? h( + 'form', + { + action: formAction, + onSubmit: () => { + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + h( + 'div', + { position: 'relative' }, + formState + ? h( + 'div', + { + style: { + position: 'absolute', + left: 0, + right: 0, + color: formState.status === 'error' ? 'red' : 'green', + fontSize: '0.75rem', + fontWeight: 'normal', + }, + }, + formState.message, + ) + : null, + ), + ) +} diff --git a/exercises/05.actions/02.solution.client/ui/error-boundary.js b/exercises/05.actions/02.solution.client/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/05.actions/02.solution.client/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/05.actions/02.solution.client/ui/img-utils.js b/exercises/05.actions/02.solution.client/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/05.actions/02.solution.client/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/05.actions/02.solution.client/ui/img.js b/exercises/05.actions/02.solution.client/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/05.actions/02.solution.client/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/05.actions/02.solution.client/ui/index.js b/exercises/05.actions/02.solution.client/ui/index.js new file mode 100644 index 0000000..9c90768 --- /dev/null +++ b/exercises/05.actions/02.solution.client/ui/index.js @@ -0,0 +1,144 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useEffect, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { contentCache, useContentCache, generateKey } from './content-cache.js' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + callServer, + }) +} + +async function callServer(id, args) { + const fetchPromise = fetch(`/action${getGlobalLocation()}`, { + method: 'POST', + headers: { 'rsc-action': id }, + body: await RSC.encodeReply(args), + }) + const actionResponsePromise = createFromFetch(fetchPromise) + const { returnValue } = await actionResponsePromise + return returnValue +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +let initialContentKey = window.history.state?.key +if (!initialContentKey) { + initialContentKey = generateKey() + window.history.replaceState({ key: initialContentKey }, '') +} +contentCache.set(initialContentKey, initialContentPromise) + +function Root() { + const latestNav = useRef(null) + const contentCache = useContentCache() + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentKey, setContentKey] = useState(initialContentKey) + const [isPending, startTransition] = useTransition() + + const location = useDeferredValue(nextLocation) + const contentPromise = contentCache.get(contentKey) + + useEffect(() => { + function handlePopState() { + const nextLocation = getGlobalLocation() + setNextLocation(nextLocation) + const historyKey = window.history.state?.key ?? generateKey() + + if (!contentCache.has(historyKey)) { + const fetchPromise = fetchContent(nextLocation) + const nextContentPromise = createFromFetch(fetchPromise) + contentCache.set(historyKey, nextContentPromise) + } + + startTransition(() => setContentKey(historyKey)) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, [contentCache]) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + const newContentKey = generateKey() + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + window.history.replaceState({ key: newContentKey }, '', nextLocation) + } else { + window.history.pushState({ key: newContentKey }, '', nextLocation) + } + return response + }), + ) + + contentCache.set(newContentKey, nextContentPromise) + startTransition(() => setContentKey(newContentKey)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/05.actions/02.solution.client/ui/router.js b/exercises/05.actions/02.solution.client/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/05.actions/02.solution.client/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/05.actions/02.solution.client/ui/ship-details-pending.js b/exercises/05.actions/02.solution.client/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/05.actions/02.solution.client/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/05.actions/02.solution.client/ui/ship-details.js b/exercises/05.actions/02.solution.client/ui/ship-details.js new file mode 100644 index 0000000..cd702fa --- /dev/null +++ b/exercises/05.actions/02.solution.client/ui/ship-details.js @@ -0,0 +1,115 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { updateShipName } from './actions.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + action: updateShipName, + initialValue: 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/05.actions/02.solution.client/ui/ship-search-results.js b/exercises/05.actions/02.solution.client/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/05.actions/02.solution.client/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/05.actions/02.solution.client/ui/ship-search.js b/exercises/05.actions/02.solution.client/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/05.actions/02.solution.client/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/05.actions/02.solution.client/ui/spin-delay.js b/exercises/05.actions/02.solution.client/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/05.actions/02.solution.client/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/05.actions/03.problem.server/.gitignore b/exercises/05.actions/03.problem.server/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/05.actions/03.problem.server/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/05.actions/03.problem.server/README.mdx b/exercises/05.actions/03.problem.server/README.mdx new file mode 100644 index 0000000..2675c67 --- /dev/null +++ b/exercises/05.actions/03.problem.server/README.mdx @@ -0,0 +1,24 @@ +# Server Side + + + +๐Ÿ‘จโ€๐Ÿ’ผ Now that we send that reference and any arguments to the backend when we +submit the form. We need an endpoint that will handle that POST request. + +The POST request we're making includes the id of the action in the `rsc-action` +header: + +``` +file:///Users/kentcdodds/code/epicweb-dev/react-server-components/playground/ui/actions.js#updateShipName +``` + +We can use that to find the module and module function to call in our server +handler. + +We'll be using a few server-side modules for parsing the form submission data. +As this is not a Node.js/hono.js course, some of this will be given to you. You +will be responsible for parsing out the action file to `import` and calling it +with the correct arguments. + +When you're finished with this one you should finally be able to submit the +form and it should work! diff --git a/exercises/05.actions/03.problem.server/db/ship-api.js b/exercises/05.actions/03.problem.server/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/05.actions/03.problem.server/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/05.actions/03.problem.server/db/ships.json b/exercises/05.actions/03.problem.server/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/05.actions/03.problem.server/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/05.actions/03.problem.server/package.json b/exercises/05.actions/03.problem.server/package.json new file mode 100644 index 0000000..aee274e --- /dev/null +++ b/exercises/05.actions/03.problem.server/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__05.actions__sep__03.problem.server", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/05.actions/03.problem.server/public/favicon.ico b/exercises/05.actions/03.problem.server/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/favicon.ico differ diff --git a/exercises/05.actions/03.problem.server/public/favicon.svg b/exercises/05.actions/03.problem.server/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/05.actions/03.problem.server/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/05.actions/03.problem.server/public/iframe-sync.js b/exercises/05.actions/03.problem.server/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/05.actions/03.problem.server/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/05.actions/03.problem.server/public/img/broken-ship.webp b/exercises/05.actions/03.problem.server/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/broken-ship.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/fallback-ship.png b/exercises/05.actions/03.problem.server/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/fallback-ship.png differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/0268fc4817ad1.webp b/exercises/05.actions/03.problem.server/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/1ae7b4b92036b.webp b/exercises/05.actions/03.problem.server/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/1ff1991efe029.webp b/exercises/05.actions/03.problem.server/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/3ba8aa65ffe6c.webp b/exercises/05.actions/03.problem.server/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/441f7092a8d44.webp b/exercises/05.actions/03.problem.server/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/5c13d8b28a14a.webp b/exercises/05.actions/03.problem.server/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/627c497212456.webp b/exercises/05.actions/03.problem.server/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/627c497212456.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/670003aed3795.webp b/exercises/05.actions/03.problem.server/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/670003aed3795.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/6c86fca8b9086.webp b/exercises/05.actions/03.problem.server/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/6f375578ead88.webp b/exercises/05.actions/03.problem.server/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/ab267a5984523.webp b/exercises/05.actions/03.problem.server/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/b442531ea32b2.webp b/exercises/05.actions/03.problem.server/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/bc4cbadf89bd3.webp b/exercises/05.actions/03.problem.server/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/cb03cc4e5717e.webp b/exercises/05.actions/03.problem.server/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/cfd10fcd2de6c.webp b/exercises/05.actions/03.problem.server/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/d3b8aa65ffe6c.webp b/exercises/05.actions/03.problem.server/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/d486d48b82b81.webp b/exercises/05.actions/03.problem.server/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/e92cefe4f6727.webp b/exercises/05.actions/03.problem.server/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/ec7a3f950f99f.webp b/exercises/05.actions/03.problem.server/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/f3d9a88e1c234.webp b/exercises/05.actions/03.problem.server/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/05.actions/03.problem.server/public/img/ships/fdc13cb488bf1.webp b/exercises/05.actions/03.problem.server/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/05.actions/03.problem.server/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/05.actions/03.problem.server/public/index.html b/exercises/05.actions/03.problem.server/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/05.actions/03.problem.server/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/05.actions/03.problem.server/public/style.css b/exercises/05.actions/03.problem.server/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/05.actions/03.problem.server/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/05.actions/03.problem.server/server/app.js b/exercises/05.actions/03.problem.server/server/app.js new file mode 100644 index 0000000..9620fce --- /dev/null +++ b/exercises/05.actions/03.problem.server/server/app.js @@ -0,0 +1,104 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { + renderToPipeableStream, + // ๐Ÿ’ฐ you'll need this + // decodeReply, +} from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +const moduleBasePath = new URL('../ui', import.meta.url).href + +// ๐Ÿจ add a returnValue argument here +async function renderApp(context) { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const root = h(App) + // ๐Ÿจ change the payload to an object that has { root, returnValue } + // ๐Ÿฆ‰ this will break the app until you update the ui/index.js file! + const payload = root + const { pipe } = renderToPipeableStream(payload, moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +} + +app.get('/rsc/:shipId?', async (context) => await renderApp(context)) + +// ๐Ÿจ add an app.post to handle POST requests to /action/:shipId? +// ๐Ÿ’ฐ This isn't a hono.js workshop, so this'll get you started: +// app.post('/action/:shipId?', async context => {}) +// ๐Ÿจ in the body of the POST handler, you'll want to: +// 1. get the serverReference from the rsc-action header (๐Ÿ’ฐ context.req.header('rsc-action')) +// 2. split the serverReference by '#' to get the filepath and export name +// 3. dynamically import the action from the filepath and name +// ๐Ÿ’ฐ (await import(filepath))[name] +// ๐Ÿ’ฏ Bonus: validate the action is a valid server reference. console.log(action.$$typeof) to see how you might determine that +// 4. get the formData object from the quest (๐Ÿ’ฐ await context.req.formData()) +// 5. decode the reply from the formData object (await decodeReply(formData, moduleBasePath)) +// 6. call the action with the ...args +// 7. call renderApp with the context and the returnValue of the action + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/05.actions/03.problem.server/server/async-storage.js b/exercises/05.actions/03.problem.server/server/async-storage.js new file mode 100644 index 0000000..b9a08b1 --- /dev/null +++ b/exercises/05.actions/03.problem.server/server/async-storage.js @@ -0,0 +1,3 @@ +import { AsyncLocalStorage } from 'node:async_hooks' + +export const shipDataStorage = new AsyncLocalStorage() diff --git a/exercises/05.actions/03.problem.server/server/register-rsc-loader.js b/exercises/05.actions/03.problem.server/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/05.actions/03.problem.server/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/05.actions/03.problem.server/server/rsc-loader.js b/exercises/05.actions/03.problem.server/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/05.actions/03.problem.server/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/05.actions/03.problem.server/tests/playwright.config.js b/exercises/05.actions/03.problem.server/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/05.actions/03.problem.server/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/05.actions/03.problem.server/tests/server-action.test.js b/exercises/05.actions/03.problem.server/tests/server-action.test.js new file mode 100644 index 0000000..1918f13 --- /dev/null +++ b/exercises/05.actions/03.problem.server/tests/server-action.test.js @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test' +import { searchShips } from '../db/ship-api.js' + +test('Submitting the form posts to the action endpoint correctly', async ({ + page, +}) => { + const { + ships: [ship], + } = await searchShips({ search: '' }) + await page.goto(`/${ship.id}`) + await page.waitForLoadState('networkidle') + + await page.getByRole('button', { name: ship.name }).click() + + const newName = `${ship.name} ${Math.random().toString(16).slice(2, 5)}` + + // Change the value of the input + await page.getByRole('textbox', { name: 'Ship Name' }).fill(newName) + + // Intercept the request to /action + const actionRequest = page.waitForRequest((request) => { + return request.url().includes('/action') && request.method() === 'POST' + }) + + // Press Enter + await page.keyboard.press('Enter') + + // Wait for the request to be made + const request = await actionRequest + + // Verify the request URL + expect(request.url()).toContain(`/action/${ship.id}`) + + const response = await request.response() + expect(response.status(), '๐Ÿšจ have you made the action endpoint yet?').toBe( + 200, + ) + + // Verify the form data payload + const postData = await request.postData() + expect(postData).toContain(newName) + expect(postData).toContain(ship.id) + + // Verify the rsc-action header + const headers = request.headers() + expect(headers['rsc-action']).toContain('ui/actions.js#updateShipName') + + // Verify the response body + const responseBody = await response.text() + // it should have a returnValue and a root + expect(responseBody).toContain('returnValue') + expect(responseBody).toContain('root') + // It should have the success message in the return value + expect(responseBody).toContain('Success!') + // And it should have updated the ship name + expect(responseBody).toContain(newName) +}) diff --git a/exercises/05.actions/03.problem.server/tests/smoke.test.js b/exercises/05.actions/03.problem.server/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/05.actions/03.problem.server/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/05.actions/03.problem.server/ui/actions.js b/exercises/05.actions/03.problem.server/ui/actions.js new file mode 100644 index 0000000..71008e9 --- /dev/null +++ b/exercises/05.actions/03.problem.server/ui/actions.js @@ -0,0 +1,15 @@ +'use server' + +import * as db from '../db/ship-api.js' + +export async function updateShipName(previousState, formData) { + try { + await db.updateShipName({ + shipId: formData.get('shipId'), + shipName: formData.get('shipName'), + }) + return { status: 'success', message: 'Success!' } + } catch (error) { + return { status: 'error', message: error?.message || String(error) } + } +} diff --git a/exercises/05.actions/03.problem.server/ui/app.js b/exercises/05.actions/03.problem.server/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/05.actions/03.problem.server/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/05.actions/03.problem.server/ui/content-cache.js b/exercises/05.actions/03.problem.server/ui/content-cache.js new file mode 100644 index 0000000..612b97f --- /dev/null +++ b/exercises/05.actions/03.problem.server/ui/content-cache.js @@ -0,0 +1,46 @@ +import { useSyncExternalStore } from 'react' + +/** + * This will call the given callback function whenever the contents of the map + * change. + */ +class ObservableMap extends Map { + listeners = new Set() + set(key, value) { + const result = super.set(key, value) + this.emitChange() + return result + } + delete(key) { + const result = super.delete(key) + this.emitChange() + return result + } + emitChange() { + for (const listener of this.listeners) { + listener() + } + } + subscribe(listener) { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } +} + +export const contentCache = new ObservableMap() + +export function generateKey() { + return Date.now().toString(36) + Math.random().toString(36).slice(2) +} + +export function useContentCache() { + function subscribe(cb) { + return contentCache.subscribe(cb) + } + function getSnapshot() { + return contentCache + } + return useSyncExternalStore(subscribe, getSnapshot) +} diff --git a/exercises/05.actions/03.problem.server/ui/edit-text.js b/exercises/05.actions/03.problem.server/ui/edit-text.js new file mode 100644 index 0000000..1904aad --- /dev/null +++ b/exercises/05.actions/03.problem.server/ui/edit-text.js @@ -0,0 +1,105 @@ +'use client' + +import { createElement as h, useRef, useState, useActionState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, action, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const [formState, formAction, isPending] = useActionState(action) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + { style: { opacity: isPending ? 0.6 : 1 } }, + edit + ? h( + 'form', + { + action: formAction, + onSubmit: () => { + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + h( + 'div', + { position: 'relative' }, + formState + ? h( + 'div', + { + style: { + position: 'absolute', + left: 0, + right: 0, + color: formState.status === 'error' ? 'red' : 'green', + fontSize: '0.75rem', + fontWeight: 'normal', + }, + }, + formState.message, + ) + : null, + ), + ) +} diff --git a/exercises/05.actions/03.problem.server/ui/error-boundary.js b/exercises/05.actions/03.problem.server/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/05.actions/03.problem.server/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/05.actions/03.problem.server/ui/img-utils.js b/exercises/05.actions/03.problem.server/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/05.actions/03.problem.server/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/05.actions/03.problem.server/ui/img.js b/exercises/05.actions/03.problem.server/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/05.actions/03.problem.server/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/05.actions/03.problem.server/ui/index.js b/exercises/05.actions/03.problem.server/ui/index.js new file mode 100644 index 0000000..d92bed1 --- /dev/null +++ b/exercises/05.actions/03.problem.server/ui/index.js @@ -0,0 +1,146 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useEffect, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { contentCache, useContentCache, generateKey } from './content-cache.js' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + callServer, + }) +} + +async function callServer(id, args) { + const fetchPromise = fetch(`/action${getGlobalLocation()}`, { + method: 'POST', + headers: { 'rsc-action': id }, + body: await RSC.encodeReply(args), + }) + const actionResponsePromise = createFromFetch(fetchPromise) + const { returnValue } = await actionResponsePromise + return returnValue +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +let initialContentKey = window.history.state?.key +if (!initialContentKey) { + initialContentKey = generateKey() + window.history.replaceState({ key: initialContentKey }, '') +} +contentCache.set(initialContentKey, initialContentPromise) + +function Root() { + const latestNav = useRef(null) + const contentCache = useContentCache() + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentKey, setContentKey] = useState(initialContentKey) + const [isPending, startTransition] = useTransition() + + const location = useDeferredValue(nextLocation) + const contentPromise = contentCache.get(contentKey) + + useEffect(() => { + function handlePopState() { + const nextLocation = getGlobalLocation() + setNextLocation(nextLocation) + const historyKey = window.history.state?.key ?? generateKey() + + if (!contentCache.has(historyKey)) { + const fetchPromise = fetchContent(nextLocation) + const nextContentPromise = createFromFetch(fetchPromise) + contentCache.set(historyKey, nextContentPromise) + } + + startTransition(() => setContentKey(historyKey)) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, [contentCache]) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + const newContentKey = generateKey() + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + window.history.replaceState({ key: newContentKey }, '', nextLocation) + } else { + window.history.pushState({ key: newContentKey }, '', nextLocation) + } + return response + }), + ) + + contentCache.set(newContentKey, nextContentPromise) + startTransition(() => setContentKey(newContentKey)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + // ๐Ÿจ the contentPromise is now an object with a root property, update this + // to render the root: ๐Ÿ’ฐ use(contentPromise).root + use(contentPromise), + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/05.actions/03.problem.server/ui/router.js b/exercises/05.actions/03.problem.server/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/05.actions/03.problem.server/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/05.actions/03.problem.server/ui/ship-details-pending.js b/exercises/05.actions/03.problem.server/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/05.actions/03.problem.server/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/05.actions/03.problem.server/ui/ship-details.js b/exercises/05.actions/03.problem.server/ui/ship-details.js new file mode 100644 index 0000000..cd702fa --- /dev/null +++ b/exercises/05.actions/03.problem.server/ui/ship-details.js @@ -0,0 +1,115 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { updateShipName } from './actions.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + action: updateShipName, + initialValue: 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/05.actions/03.problem.server/ui/ship-search-results.js b/exercises/05.actions/03.problem.server/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/05.actions/03.problem.server/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/05.actions/03.problem.server/ui/ship-search.js b/exercises/05.actions/03.problem.server/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/05.actions/03.problem.server/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/05.actions/03.problem.server/ui/spin-delay.js b/exercises/05.actions/03.problem.server/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/05.actions/03.problem.server/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/05.actions/03.solution.server/.gitignore b/exercises/05.actions/03.solution.server/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/05.actions/03.solution.server/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/05.actions/03.solution.server/README.mdx b/exercises/05.actions/03.solution.server/README.mdx new file mode 100644 index 0000000..0f5b74b --- /dev/null +++ b/exercises/05.actions/03.solution.server/README.mdx @@ -0,0 +1,6 @@ +# Server Side + + + +๐Ÿ‘จโ€๐Ÿ’ผ You did it! Great job! Now our users can change the name of a ship. Awesome. +There's one more optimization I want to make here though. diff --git a/exercises/05.actions/03.solution.server/db/ship-api.js b/exercises/05.actions/03.solution.server/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/05.actions/03.solution.server/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/05.actions/03.solution.server/db/ships.json b/exercises/05.actions/03.solution.server/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/05.actions/03.solution.server/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/05.actions/03.solution.server/package.json b/exercises/05.actions/03.solution.server/package.json new file mode 100644 index 0000000..f4717be --- /dev/null +++ b/exercises/05.actions/03.solution.server/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__05.actions__sep__03.solution.server", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/05.actions/03.solution.server/public/favicon.ico b/exercises/05.actions/03.solution.server/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/favicon.ico differ diff --git a/exercises/05.actions/03.solution.server/public/favicon.svg b/exercises/05.actions/03.solution.server/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/05.actions/03.solution.server/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/05.actions/03.solution.server/public/iframe-sync.js b/exercises/05.actions/03.solution.server/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/05.actions/03.solution.server/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/05.actions/03.solution.server/public/img/broken-ship.webp b/exercises/05.actions/03.solution.server/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/broken-ship.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/fallback-ship.png b/exercises/05.actions/03.solution.server/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/fallback-ship.png differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/0268fc4817ad1.webp b/exercises/05.actions/03.solution.server/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/1ae7b4b92036b.webp b/exercises/05.actions/03.solution.server/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/1ff1991efe029.webp b/exercises/05.actions/03.solution.server/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/3ba8aa65ffe6c.webp b/exercises/05.actions/03.solution.server/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/441f7092a8d44.webp b/exercises/05.actions/03.solution.server/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/5c13d8b28a14a.webp b/exercises/05.actions/03.solution.server/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/627c497212456.webp b/exercises/05.actions/03.solution.server/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/627c497212456.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/670003aed3795.webp b/exercises/05.actions/03.solution.server/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/670003aed3795.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/6c86fca8b9086.webp b/exercises/05.actions/03.solution.server/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/6f375578ead88.webp b/exercises/05.actions/03.solution.server/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/ab267a5984523.webp b/exercises/05.actions/03.solution.server/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/b442531ea32b2.webp b/exercises/05.actions/03.solution.server/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/bc4cbadf89bd3.webp b/exercises/05.actions/03.solution.server/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/cb03cc4e5717e.webp b/exercises/05.actions/03.solution.server/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/cfd10fcd2de6c.webp b/exercises/05.actions/03.solution.server/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/d3b8aa65ffe6c.webp b/exercises/05.actions/03.solution.server/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/d486d48b82b81.webp b/exercises/05.actions/03.solution.server/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/e92cefe4f6727.webp b/exercises/05.actions/03.solution.server/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/ec7a3f950f99f.webp b/exercises/05.actions/03.solution.server/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/f3d9a88e1c234.webp b/exercises/05.actions/03.solution.server/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/05.actions/03.solution.server/public/img/ships/fdc13cb488bf1.webp b/exercises/05.actions/03.solution.server/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/05.actions/03.solution.server/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/05.actions/03.solution.server/public/index.html b/exercises/05.actions/03.solution.server/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/05.actions/03.solution.server/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/05.actions/03.solution.server/public/style.css b/exercises/05.actions/03.solution.server/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/05.actions/03.solution.server/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/05.actions/03.solution.server/server/app.js b/exercises/05.actions/03.solution.server/server/app.js new file mode 100644 index 0000000..f26cb41 --- /dev/null +++ b/exercises/05.actions/03.solution.server/server/app.js @@ -0,0 +1,103 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { + renderToPipeableStream, + decodeReply, +} from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +const moduleBasePath = new URL('../ui', import.meta.url).href + +async function renderApp(context, returnValue) { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const root = h(App) + const payload = { root, returnValue } + const { pipe } = renderToPipeableStream(payload, moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +} + +app.get('/rsc/:shipId?', async (context) => renderApp(context, null)) + +app.post('/action/:shipId?', async (context) => { + const serverReference = context.req.header('rsc-action') + const [filepath, name] = serverReference.split('#') + const action = (await import(filepath))[name] + // Validate that this is actually a function we intended to expose and + // not the client trying to invoke arbitrary functions. In a real app, + // you'd have a manifest verifying this before even importing it. + if (action.$$typeof !== Symbol.for('react.server.reference')) { + throw new Error('Invalid action') + } + + const formData = await context.req.formData() + const args = await decodeReply(formData, moduleBasePath) + const result = await action(...args) + return await renderApp(context, result) +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/05.actions/03.solution.server/server/async-storage.js b/exercises/05.actions/03.solution.server/server/async-storage.js new file mode 100644 index 0000000..b9a08b1 --- /dev/null +++ b/exercises/05.actions/03.solution.server/server/async-storage.js @@ -0,0 +1,3 @@ +import { AsyncLocalStorage } from 'node:async_hooks' + +export const shipDataStorage = new AsyncLocalStorage() diff --git a/exercises/05.actions/03.solution.server/server/register-rsc-loader.js b/exercises/05.actions/03.solution.server/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/05.actions/03.solution.server/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/05.actions/03.solution.server/server/rsc-loader.js b/exercises/05.actions/03.solution.server/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/05.actions/03.solution.server/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/05.actions/03.solution.server/tests/playwright.config.js b/exercises/05.actions/03.solution.server/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/05.actions/03.solution.server/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/05.actions/03.solution.server/tests/server-action.test.js b/exercises/05.actions/03.solution.server/tests/server-action.test.js new file mode 100644 index 0000000..1918f13 --- /dev/null +++ b/exercises/05.actions/03.solution.server/tests/server-action.test.js @@ -0,0 +1,57 @@ +import { test, expect } from '@playwright/test' +import { searchShips } from '../db/ship-api.js' + +test('Submitting the form posts to the action endpoint correctly', async ({ + page, +}) => { + const { + ships: [ship], + } = await searchShips({ search: '' }) + await page.goto(`/${ship.id}`) + await page.waitForLoadState('networkidle') + + await page.getByRole('button', { name: ship.name }).click() + + const newName = `${ship.name} ${Math.random().toString(16).slice(2, 5)}` + + // Change the value of the input + await page.getByRole('textbox', { name: 'Ship Name' }).fill(newName) + + // Intercept the request to /action + const actionRequest = page.waitForRequest((request) => { + return request.url().includes('/action') && request.method() === 'POST' + }) + + // Press Enter + await page.keyboard.press('Enter') + + // Wait for the request to be made + const request = await actionRequest + + // Verify the request URL + expect(request.url()).toContain(`/action/${ship.id}`) + + const response = await request.response() + expect(response.status(), '๐Ÿšจ have you made the action endpoint yet?').toBe( + 200, + ) + + // Verify the form data payload + const postData = await request.postData() + expect(postData).toContain(newName) + expect(postData).toContain(ship.id) + + // Verify the rsc-action header + const headers = request.headers() + expect(headers['rsc-action']).toContain('ui/actions.js#updateShipName') + + // Verify the response body + const responseBody = await response.text() + // it should have a returnValue and a root + expect(responseBody).toContain('returnValue') + expect(responseBody).toContain('root') + // It should have the success message in the return value + expect(responseBody).toContain('Success!') + // And it should have updated the ship name + expect(responseBody).toContain(newName) +}) diff --git a/exercises/05.actions/03.solution.server/tests/smoke.test.js b/exercises/05.actions/03.solution.server/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/05.actions/03.solution.server/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/05.actions/03.solution.server/ui/actions.js b/exercises/05.actions/03.solution.server/ui/actions.js new file mode 100644 index 0000000..71008e9 --- /dev/null +++ b/exercises/05.actions/03.solution.server/ui/actions.js @@ -0,0 +1,15 @@ +'use server' + +import * as db from '../db/ship-api.js' + +export async function updateShipName(previousState, formData) { + try { + await db.updateShipName({ + shipId: formData.get('shipId'), + shipName: formData.get('shipName'), + }) + return { status: 'success', message: 'Success!' } + } catch (error) { + return { status: 'error', message: error?.message || String(error) } + } +} diff --git a/exercises/05.actions/03.solution.server/ui/app.js b/exercises/05.actions/03.solution.server/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/05.actions/03.solution.server/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/05.actions/03.solution.server/ui/content-cache.js b/exercises/05.actions/03.solution.server/ui/content-cache.js new file mode 100644 index 0000000..612b97f --- /dev/null +++ b/exercises/05.actions/03.solution.server/ui/content-cache.js @@ -0,0 +1,46 @@ +import { useSyncExternalStore } from 'react' + +/** + * This will call the given callback function whenever the contents of the map + * change. + */ +class ObservableMap extends Map { + listeners = new Set() + set(key, value) { + const result = super.set(key, value) + this.emitChange() + return result + } + delete(key) { + const result = super.delete(key) + this.emitChange() + return result + } + emitChange() { + for (const listener of this.listeners) { + listener() + } + } + subscribe(listener) { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } +} + +export const contentCache = new ObservableMap() + +export function generateKey() { + return Date.now().toString(36) + Math.random().toString(36).slice(2) +} + +export function useContentCache() { + function subscribe(cb) { + return contentCache.subscribe(cb) + } + function getSnapshot() { + return contentCache + } + return useSyncExternalStore(subscribe, getSnapshot) +} diff --git a/exercises/05.actions/03.solution.server/ui/edit-text.js b/exercises/05.actions/03.solution.server/ui/edit-text.js new file mode 100644 index 0000000..1904aad --- /dev/null +++ b/exercises/05.actions/03.solution.server/ui/edit-text.js @@ -0,0 +1,105 @@ +'use client' + +import { createElement as h, useRef, useState, useActionState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, action, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const [formState, formAction, isPending] = useActionState(action) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + { style: { opacity: isPending ? 0.6 : 1 } }, + edit + ? h( + 'form', + { + action: formAction, + onSubmit: () => { + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + h( + 'div', + { position: 'relative' }, + formState + ? h( + 'div', + { + style: { + position: 'absolute', + left: 0, + right: 0, + color: formState.status === 'error' ? 'red' : 'green', + fontSize: '0.75rem', + fontWeight: 'normal', + }, + }, + formState.message, + ) + : null, + ), + ) +} diff --git a/exercises/05.actions/03.solution.server/ui/error-boundary.js b/exercises/05.actions/03.solution.server/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/05.actions/03.solution.server/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/05.actions/03.solution.server/ui/img-utils.js b/exercises/05.actions/03.solution.server/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/05.actions/03.solution.server/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/05.actions/03.solution.server/ui/img.js b/exercises/05.actions/03.solution.server/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/05.actions/03.solution.server/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/05.actions/03.solution.server/ui/index.js b/exercises/05.actions/03.solution.server/ui/index.js new file mode 100644 index 0000000..6a7fc83 --- /dev/null +++ b/exercises/05.actions/03.solution.server/ui/index.js @@ -0,0 +1,144 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useEffect, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { contentCache, useContentCache, generateKey } from './content-cache.js' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + callServer, + }) +} + +async function callServer(id, args) { + const fetchPromise = fetch(`/action${getGlobalLocation()}`, { + method: 'POST', + headers: { 'rsc-action': id }, + body: await RSC.encodeReply(args), + }) + const actionResponsePromise = createFromFetch(fetchPromise) + const { returnValue } = await actionResponsePromise + return returnValue +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +let initialContentKey = window.history.state?.key +if (!initialContentKey) { + initialContentKey = generateKey() + window.history.replaceState({ key: initialContentKey }, '') +} +contentCache.set(initialContentKey, initialContentPromise) + +function Root() { + const latestNav = useRef(null) + const contentCache = useContentCache() + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentKey, setContentKey] = useState(initialContentKey) + const [isPending, startTransition] = useTransition() + + const location = useDeferredValue(nextLocation) + const contentPromise = contentCache.get(contentKey) + + useEffect(() => { + function handlePopState() { + const nextLocation = getGlobalLocation() + setNextLocation(nextLocation) + const historyKey = window.history.state?.key ?? generateKey() + + if (!contentCache.has(historyKey)) { + const fetchPromise = fetchContent(nextLocation) + const nextContentPromise = createFromFetch(fetchPromise) + contentCache.set(historyKey, nextContentPromise) + } + + startTransition(() => setContentKey(historyKey)) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, [contentCache]) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + const newContentKey = generateKey() + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + window.history.replaceState({ key: newContentKey }, '', nextLocation) + } else { + window.history.pushState({ key: newContentKey }, '', nextLocation) + } + return response + }), + ) + + contentCache.set(newContentKey, nextContentPromise) + startTransition(() => setContentKey(newContentKey)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise).root, + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/05.actions/03.solution.server/ui/router.js b/exercises/05.actions/03.solution.server/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/05.actions/03.solution.server/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/05.actions/03.solution.server/ui/ship-details-pending.js b/exercises/05.actions/03.solution.server/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/05.actions/03.solution.server/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/05.actions/03.solution.server/ui/ship-details.js b/exercises/05.actions/03.solution.server/ui/ship-details.js new file mode 100644 index 0000000..cd702fa --- /dev/null +++ b/exercises/05.actions/03.solution.server/ui/ship-details.js @@ -0,0 +1,115 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { updateShipName } from './actions.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + action: updateShipName, + initialValue: 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/05.actions/03.solution.server/ui/ship-search-results.js b/exercises/05.actions/03.solution.server/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/05.actions/03.solution.server/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/05.actions/03.solution.server/ui/ship-search.js b/exercises/05.actions/03.solution.server/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/05.actions/03.solution.server/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/05.actions/03.solution.server/ui/spin-delay.js b/exercises/05.actions/03.solution.server/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/05.actions/03.solution.server/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/05.actions/04.problem.revalidation/.gitignore b/exercises/05.actions/04.problem.revalidation/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/05.actions/04.problem.revalidation/README.mdx b/exercises/05.actions/04.problem.revalidation/README.mdx new file mode 100644 index 0000000..1165159 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/README.mdx @@ -0,0 +1,88 @@ +# Revalidation + + + +๐Ÿ‘จโ€๐Ÿ’ผ You might notice that once we make a change to the ship name, the list on the +left doesn't update immediately. We are actually sending the updated UI so +what's the problem? + +The problem is we're ignoring the RSC payload. We're only doing something with +the `returnValue`. Instead, we need to use the `root` as well to update our +component to use that value. + +But here's the tricky thing. The `callServer` function is outside our component +so how can we call `setContentKey` within our `startTransition`? + +We use fancy JavaScript tricks of course! Here's an example of what I mean: + +```js +function increment() { + throw new Error('This was called before Counter rendered') +} + +function Counter() { + const [count, setCount] = useState(0) + + useEffect(() => { + increment = () => setCount((c) => c + 1) + }, []) + + return +} +``` + +With this setup, you can call `increment` from anywhere in your code and it will +update the `Counter` component. This is because we're using a closure to keep +the `setCount` function around. + +This totally breaks reusability of the component, but in our case we only have +a single `Root` component anyway, so it's perfectly safe. + +Another challenge we're going to have in this bit is we need to only update the +cached value after the response has finished streaming, otherwise we'll just +render the pending UI right away which would be annoying. + +You might think about doing something like this: + +```js +const actionResponse = await createFromFetch(fetchPromise) +contentCache.set(contentKey, actionResponse) +updateContentKey(contentKey) +return actionResponse.returnValue +``` + +Unfortunately, this won't work because the promise resolves as soon as the +stream **starts** not when it ends. So we need something a little more fancy: + +```js +function onStreamFinished(fetchPromise, onFinished) { + // create a promise chain that resolves when the stream is completely consumed + return ( + fetchPromise + // clone the response so createFromFetch can use it (otherwise we lock the reader) + // and wait for the text to be consumed so we know the stream is finished + .then((response) => response.clone().text()) + .then(onFinished) + ) +} +``` + +Basically, we're cloning the response so we can read the text from it without +locking the reader (otherwise we'd prevent `createFromFetch` from using it). +This way we can know when the stream is finished and update: + +```js +const fetchPromise = fetch(/*...*/) +onStreamFinished(fetchPromise, () => { + updateContentKey(contentKey) +}) +const actionResponsePromise = createFromFetch(fetchPromise) +// ... etc +``` + +It's important that you call `onStreamFinished` before calling `createFromFetch` +so we can make a copy of the response before `createFromFetch` starts consuming +it. + +So yeah, doing a couple interesting things in this one, but it should result in +a great user experience! Enjoy! diff --git a/exercises/05.actions/04.problem.revalidation/db/ship-api.js b/exercises/05.actions/04.problem.revalidation/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/05.actions/04.problem.revalidation/db/ships.json b/exercises/05.actions/04.problem.revalidation/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/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/05.actions/04.problem.revalidation/package.json b/exercises/05.actions/04.problem.revalidation/package.json new file mode 100644 index 0000000..989ca02 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__05.actions__sep__04.problem.revalidation", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/05.actions/04.problem.revalidation/public/favicon.ico b/exercises/05.actions/04.problem.revalidation/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/favicon.ico differ diff --git a/exercises/05.actions/04.problem.revalidation/public/favicon.svg b/exercises/05.actions/04.problem.revalidation/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/05.actions/04.problem.revalidation/public/iframe-sync.js b/exercises/05.actions/04.problem.revalidation/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/05.actions/04.problem.revalidation/public/img/broken-ship.webp b/exercises/05.actions/04.problem.revalidation/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/broken-ship.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/fallback-ship.png b/exercises/05.actions/04.problem.revalidation/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/fallback-ship.png differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/0268fc4817ad1.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/1ae7b4b92036b.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/1ff1991efe029.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/3ba8aa65ffe6c.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/441f7092a8d44.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/5c13d8b28a14a.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/627c497212456.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/627c497212456.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/670003aed3795.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/670003aed3795.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/6c86fca8b9086.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/6f375578ead88.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/ab267a5984523.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/b442531ea32b2.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/bc4cbadf89bd3.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/cb03cc4e5717e.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/cfd10fcd2de6c.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/d3b8aa65ffe6c.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/d486d48b82b81.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/e92cefe4f6727.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/ec7a3f950f99f.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/f3d9a88e1c234.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/img/ships/fdc13cb488bf1.webp b/exercises/05.actions/04.problem.revalidation/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/05.actions/04.problem.revalidation/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/05.actions/04.problem.revalidation/public/index.html b/exercises/05.actions/04.problem.revalidation/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/05.actions/04.problem.revalidation/public/style.css b/exercises/05.actions/04.problem.revalidation/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/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/05.actions/04.problem.revalidation/server/app.js b/exercises/05.actions/04.problem.revalidation/server/app.js new file mode 100644 index 0000000..f26cb41 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/server/app.js @@ -0,0 +1,103 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { + renderToPipeableStream, + decodeReply, +} from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +const moduleBasePath = new URL('../ui', import.meta.url).href + +async function renderApp(context, returnValue) { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const root = h(App) + const payload = { root, returnValue } + const { pipe } = renderToPipeableStream(payload, moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +} + +app.get('/rsc/:shipId?', async (context) => renderApp(context, null)) + +app.post('/action/:shipId?', async (context) => { + const serverReference = context.req.header('rsc-action') + const [filepath, name] = serverReference.split('#') + const action = (await import(filepath))[name] + // Validate that this is actually a function we intended to expose and + // not the client trying to invoke arbitrary functions. In a real app, + // you'd have a manifest verifying this before even importing it. + if (action.$$typeof !== Symbol.for('react.server.reference')) { + throw new Error('Invalid action') + } + + const formData = await context.req.formData() + const args = await decodeReply(formData, moduleBasePath) + const result = await action(...args) + return await renderApp(context, result) +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/05.actions/04.problem.revalidation/server/async-storage.js b/exercises/05.actions/04.problem.revalidation/server/async-storage.js new file mode 100644 index 0000000..b9a08b1 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/server/async-storage.js @@ -0,0 +1,3 @@ +import { AsyncLocalStorage } from 'node:async_hooks' + +export const shipDataStorage = new AsyncLocalStorage() diff --git a/exercises/05.actions/04.problem.revalidation/server/register-rsc-loader.js b/exercises/05.actions/04.problem.revalidation/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/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/05.actions/04.problem.revalidation/server/rsc-loader.js b/exercises/05.actions/04.problem.revalidation/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/05.actions/04.problem.revalidation/tests/playwright.config.js b/exercises/05.actions/04.problem.revalidation/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/05.actions/04.problem.revalidation/tests/revalidation.test.js b/exercises/05.actions/04.problem.revalidation/tests/revalidation.test.js new file mode 100644 index 0000000..b96e550 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/tests/revalidation.test.js @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test' +import { searchShips } from '../db/ship-api.js' + +test('Submitting the form posts to the action endpoint correctly', async ({ + page, +}) => { + const { + ships: [ship], + } = await searchShips({ search: '' }) + await page.goto(`/${ship.id}`) + await page.waitForLoadState('networkidle') + + await page.getByRole('button', { name: ship.name }).click() + + const newName = `${ship.name} ${Math.random().toString(16).slice(2, 5)}` + + // Change the value of the input + await page.getByRole('textbox', { name: 'Ship Name' }).fill(newName) + + // Intercept the request to /action + const actionRequest = page.waitForRequest((request) => { + return request.url().includes('/action') && request.method() === 'POST' + }) + + // Press Enter + await page.keyboard.press('Enter') + + // Wait for the request to be made + const request = await actionRequest + + // Verify the request URL + expect(request.url()).toContain(`/action/${ship.id}`) + + const response = await request.response() + expect(response.status(), '๐Ÿšจ have you made the action endpoint yet?').toBe( + 200, + ) + + // Verify the form data payload + const postData = await request.postData() + expect(postData).toContain(newName) + expect(postData).toContain(ship.id) + + // Verify the rsc-action header + const headers = request.headers() + expect(headers['rsc-action']).toContain('ui/actions.js#updateShipName') + + // Verify the response body + const responseBody = await response.text() + // it should have a returnValue and a root + expect(responseBody).toContain('returnValue') + expect(responseBody).toContain('root') + // It should have the success message in the return value + expect(responseBody).toContain('Success!') + // And it should have updated the ship name + expect(responseBody).toContain(newName) + + // Verify the name of the ship in the list has been updated + await expect( + await page.getByRole('list').getByText(newName), + '๐Ÿšจ the list of ships has not been updated. Make sure to update the RSC content with the root returned from the action call', + ).toBeVisible() +}) diff --git a/exercises/05.actions/04.problem.revalidation/tests/smoke.test.js b/exercises/05.actions/04.problem.revalidation/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/05.actions/04.problem.revalidation/ui/actions.js b/exercises/05.actions/04.problem.revalidation/ui/actions.js new file mode 100644 index 0000000..71008e9 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/ui/actions.js @@ -0,0 +1,15 @@ +'use server' + +import * as db from '../db/ship-api.js' + +export async function updateShipName(previousState, formData) { + try { + await db.updateShipName({ + shipId: formData.get('shipId'), + shipName: formData.get('shipName'), + }) + return { status: 'success', message: 'Success!' } + } catch (error) { + return { status: 'error', message: error?.message || String(error) } + } +} diff --git a/exercises/05.actions/04.problem.revalidation/ui/app.js b/exercises/05.actions/04.problem.revalidation/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/05.actions/04.problem.revalidation/ui/content-cache.js b/exercises/05.actions/04.problem.revalidation/ui/content-cache.js new file mode 100644 index 0000000..612b97f --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/ui/content-cache.js @@ -0,0 +1,46 @@ +import { useSyncExternalStore } from 'react' + +/** + * This will call the given callback function whenever the contents of the map + * change. + */ +class ObservableMap extends Map { + listeners = new Set() + set(key, value) { + const result = super.set(key, value) + this.emitChange() + return result + } + delete(key) { + const result = super.delete(key) + this.emitChange() + return result + } + emitChange() { + for (const listener of this.listeners) { + listener() + } + } + subscribe(listener) { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } +} + +export const contentCache = new ObservableMap() + +export function generateKey() { + return Date.now().toString(36) + Math.random().toString(36).slice(2) +} + +export function useContentCache() { + function subscribe(cb) { + return contentCache.subscribe(cb) + } + function getSnapshot() { + return contentCache + } + return useSyncExternalStore(subscribe, getSnapshot) +} diff --git a/exercises/05.actions/04.problem.revalidation/ui/edit-text.js b/exercises/05.actions/04.problem.revalidation/ui/edit-text.js new file mode 100644 index 0000000..1904aad --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/ui/edit-text.js @@ -0,0 +1,105 @@ +'use client' + +import { createElement as h, useRef, useState, useActionState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, action, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const [formState, formAction, isPending] = useActionState(action) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + { style: { opacity: isPending ? 0.6 : 1 } }, + edit + ? h( + 'form', + { + action: formAction, + onSubmit: () => { + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + h( + 'div', + { position: 'relative' }, + formState + ? h( + 'div', + { + style: { + position: 'absolute', + left: 0, + right: 0, + color: formState.status === 'error' ? 'red' : 'green', + fontSize: '0.75rem', + fontWeight: 'normal', + }, + }, + formState.message, + ) + : null, + ), + ) +} diff --git a/exercises/05.actions/04.problem.revalidation/ui/error-boundary.js b/exercises/05.actions/04.problem.revalidation/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/05.actions/04.problem.revalidation/ui/img-utils.js b/exercises/05.actions/04.problem.revalidation/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/05.actions/04.problem.revalidation/ui/img.js b/exercises/05.actions/04.problem.revalidation/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/05.actions/04.problem.revalidation/ui/index.js b/exercises/05.actions/04.problem.revalidation/ui/index.js new file mode 100644 index 0000000..314aee4 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/ui/index.js @@ -0,0 +1,176 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useEffect, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { contentCache, useContentCache, generateKey } from './content-cache.js' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +// ๐Ÿจ uncomment this. We're going to reassign this function when our component +// renders, but we need it here so we can call it from outside our component. +// function updateContentKey() { +// console.error('updateContentKey called before it was set!') +// } + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + callServer, + }) +} + +async function callServer(id, args) { + const fetchPromise = fetch(`/action${getGlobalLocation()}`, { + method: 'POST', + headers: { 'rsc-action': id }, + body: await RSC.encodeReply(args), + }) + // ๐Ÿจ get the contentKey from window.history.state?.key ?? generateKey() + // ๐Ÿจ use the onStreamFinished utility from below: + // ๐Ÿ’ฐ + // onStreamFinished(fetchPromise, () => { + // ๐Ÿจ when the stream is finished, call updateContentKey with the contentKey + // }) + // ๐Ÿฆ‰ we need to wait until the stream is finished otherwise we'll update to a + // pending state! + const actionResponsePromise = createFromFetch(fetchPromise) + // ๐Ÿจ use the contentKey to add the actionResponsePromise in the contentCache + const { returnValue } = await actionResponsePromise + return returnValue +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +let initialContentKey = window.history.state?.key +if (!initialContentKey) { + initialContentKey = generateKey() + window.history.replaceState({ key: initialContentKey }, '') +} +contentCache.set(initialContentKey, initialContentPromise) + +// ๐Ÿ’ฐ you're going to want this handy utility +// function onStreamFinished(fetchPromise, onFinished) { +// // create a promise chain that resolves when the stream is completely consumed +// return ( +// fetchPromise +// // clone the response so createFromFetch can use it (otherwise we lock the reader) +// // and wait for the text to be consumed so we know the stream is finished +// .then(response => response.clone().text()) +// .then(onFinished) +// ) +// } + +function Root() { + const latestNav = useRef(null) + const contentCache = useContentCache() + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentKey, setContentKey] = useState(initialContentKey) + const [isPending, startTransition] = useTransition() + + // ๐Ÿจ add a useEffect here that reassigns updateContentKey to a function that + // accepts a newContentKey and calls setContentKey(newContentKey) in a startTransition + + const location = useDeferredValue(nextLocation) + const contentPromise = contentCache.get(contentKey) + + useEffect(() => { + function handlePopState() { + const nextLocation = getGlobalLocation() + setNextLocation(nextLocation) + const historyKey = window.history.state?.key ?? generateKey() + + if (!contentCache.has(historyKey)) { + const fetchPromise = fetchContent(nextLocation) + const nextContentPromise = createFromFetch(fetchPromise) + contentCache.set(historyKey, nextContentPromise) + } + + // ๐Ÿจ swap this with updateContentKey + startTransition(() => setContentKey(historyKey)) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, [contentCache]) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + const newContentKey = generateKey() + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + window.history.replaceState({ key: newContentKey }, '', nextLocation) + } else { + window.history.pushState({ key: newContentKey }, '', nextLocation) + } + return response + }), + ) + + contentCache.set(newContentKey, nextContentPromise) + // ๐Ÿจ swap this with updateContentKey + startTransition(() => setContentKey(newContentKey)) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise).root, + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/05.actions/04.problem.revalidation/ui/router.js b/exercises/05.actions/04.problem.revalidation/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/05.actions/04.problem.revalidation/ui/ship-details-pending.js b/exercises/05.actions/04.problem.revalidation/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/05.actions/04.problem.revalidation/ui/ship-details.js b/exercises/05.actions/04.problem.revalidation/ui/ship-details.js new file mode 100644 index 0000000..cd702fa --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/ui/ship-details.js @@ -0,0 +1,115 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { updateShipName } from './actions.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + action: updateShipName, + initialValue: 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/05.actions/04.problem.revalidation/ui/ship-search-results.js b/exercises/05.actions/04.problem.revalidation/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/05.actions/04.problem.revalidation/ui/ship-search.js b/exercises/05.actions/04.problem.revalidation/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/05.actions/04.problem.revalidation/ui/spin-delay.js b/exercises/05.actions/04.problem.revalidation/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/05.actions/04.problem.revalidation/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/05.actions/04.solution.revalidation/.gitignore b/exercises/05.actions/04.solution.revalidation/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/05.actions/04.solution.revalidation/README.mdx b/exercises/05.actions/04.solution.revalidation/README.mdx new file mode 100644 index 0000000..d41a611 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/README.mdx @@ -0,0 +1,14 @@ +# Revalidation + + + +๐Ÿ‘จโ€๐Ÿ’ผ Great job! Now we're getting automatic revalidation of our UI as part of our +actions. This is a great way to ensure that our UI is always up-to-date with the +latest data. + +What's really cool about this is it applies to all of our actions. Additionally +this simulates the behavior of the web platform. Whenever you submit a form, the +browser will trigger a full page refresh by default, effectively revalidating +your entire page. We're doing the same thing, just more efficiently. + +Good job! diff --git a/exercises/05.actions/04.solution.revalidation/db/ship-api.js b/exercises/05.actions/04.solution.revalidation/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/05.actions/04.solution.revalidation/db/ships.json b/exercises/05.actions/04.solution.revalidation/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/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/05.actions/04.solution.revalidation/package.json b/exercises/05.actions/04.solution.revalidation/package.json new file mode 100644 index 0000000..b2f3a42 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__05.actions__sep__04.solution.revalidation", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/05.actions/04.solution.revalidation/public/favicon.ico b/exercises/05.actions/04.solution.revalidation/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/favicon.ico differ diff --git a/exercises/05.actions/04.solution.revalidation/public/favicon.svg b/exercises/05.actions/04.solution.revalidation/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/05.actions/04.solution.revalidation/public/iframe-sync.js b/exercises/05.actions/04.solution.revalidation/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/05.actions/04.solution.revalidation/public/img/broken-ship.webp b/exercises/05.actions/04.solution.revalidation/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/broken-ship.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/fallback-ship.png b/exercises/05.actions/04.solution.revalidation/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/fallback-ship.png differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/0268fc4817ad1.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/1ae7b4b92036b.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/1ff1991efe029.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/3ba8aa65ffe6c.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/441f7092a8d44.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/5c13d8b28a14a.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/627c497212456.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/627c497212456.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/670003aed3795.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/670003aed3795.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/6c86fca8b9086.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/6f375578ead88.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/ab267a5984523.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/b442531ea32b2.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/bc4cbadf89bd3.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/cb03cc4e5717e.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/cfd10fcd2de6c.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/d3b8aa65ffe6c.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/d486d48b82b81.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/e92cefe4f6727.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/ec7a3f950f99f.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/f3d9a88e1c234.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/img/ships/fdc13cb488bf1.webp b/exercises/05.actions/04.solution.revalidation/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/05.actions/04.solution.revalidation/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/05.actions/04.solution.revalidation/public/index.html b/exercises/05.actions/04.solution.revalidation/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/05.actions/04.solution.revalidation/public/style.css b/exercises/05.actions/04.solution.revalidation/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/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/05.actions/04.solution.revalidation/server/app.js b/exercises/05.actions/04.solution.revalidation/server/app.js new file mode 100644 index 0000000..f26cb41 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/server/app.js @@ -0,0 +1,103 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { + renderToPipeableStream, + decodeReply, +} from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +const moduleBasePath = new URL('../ui', import.meta.url).href + +async function renderApp(context, returnValue) { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const root = h(App) + const payload = { root, returnValue } + const { pipe } = renderToPipeableStream(payload, moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +} + +app.get('/rsc/:shipId?', async (context) => renderApp(context, null)) + +app.post('/action/:shipId?', async (context) => { + const serverReference = context.req.header('rsc-action') + const [filepath, name] = serverReference.split('#') + const action = (await import(filepath))[name] + // Validate that this is actually a function we intended to expose and + // not the client trying to invoke arbitrary functions. In a real app, + // you'd have a manifest verifying this before even importing it. + if (action.$$typeof !== Symbol.for('react.server.reference')) { + throw new Error('Invalid action') + } + + const formData = await context.req.formData() + const args = await decodeReply(formData, moduleBasePath) + const result = await action(...args) + return await renderApp(context, result) +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/05.actions/04.solution.revalidation/server/async-storage.js b/exercises/05.actions/04.solution.revalidation/server/async-storage.js new file mode 100644 index 0000000..b9a08b1 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/server/async-storage.js @@ -0,0 +1,3 @@ +import { AsyncLocalStorage } from 'node:async_hooks' + +export const shipDataStorage = new AsyncLocalStorage() diff --git a/exercises/05.actions/04.solution.revalidation/server/register-rsc-loader.js b/exercises/05.actions/04.solution.revalidation/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/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/05.actions/04.solution.revalidation/server/rsc-loader.js b/exercises/05.actions/04.solution.revalidation/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/05.actions/04.solution.revalidation/tests/playwright.config.js b/exercises/05.actions/04.solution.revalidation/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/05.actions/04.solution.revalidation/tests/revalidation.test.js b/exercises/05.actions/04.solution.revalidation/tests/revalidation.test.js new file mode 100644 index 0000000..b96e550 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/tests/revalidation.test.js @@ -0,0 +1,63 @@ +import { test, expect } from '@playwright/test' +import { searchShips } from '../db/ship-api.js' + +test('Submitting the form posts to the action endpoint correctly', async ({ + page, +}) => { + const { + ships: [ship], + } = await searchShips({ search: '' }) + await page.goto(`/${ship.id}`) + await page.waitForLoadState('networkidle') + + await page.getByRole('button', { name: ship.name }).click() + + const newName = `${ship.name} ${Math.random().toString(16).slice(2, 5)}` + + // Change the value of the input + await page.getByRole('textbox', { name: 'Ship Name' }).fill(newName) + + // Intercept the request to /action + const actionRequest = page.waitForRequest((request) => { + return request.url().includes('/action') && request.method() === 'POST' + }) + + // Press Enter + await page.keyboard.press('Enter') + + // Wait for the request to be made + const request = await actionRequest + + // Verify the request URL + expect(request.url()).toContain(`/action/${ship.id}`) + + const response = await request.response() + expect(response.status(), '๐Ÿšจ have you made the action endpoint yet?').toBe( + 200, + ) + + // Verify the form data payload + const postData = await request.postData() + expect(postData).toContain(newName) + expect(postData).toContain(ship.id) + + // Verify the rsc-action header + const headers = request.headers() + expect(headers['rsc-action']).toContain('ui/actions.js#updateShipName') + + // Verify the response body + const responseBody = await response.text() + // it should have a returnValue and a root + expect(responseBody).toContain('returnValue') + expect(responseBody).toContain('root') + // It should have the success message in the return value + expect(responseBody).toContain('Success!') + // And it should have updated the ship name + expect(responseBody).toContain(newName) + + // Verify the name of the ship in the list has been updated + await expect( + await page.getByRole('list').getByText(newName), + '๐Ÿšจ the list of ships has not been updated. Make sure to update the RSC content with the root returned from the action call', + ).toBeVisible() +}) diff --git a/exercises/05.actions/04.solution.revalidation/tests/smoke.test.js b/exercises/05.actions/04.solution.revalidation/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/05.actions/04.solution.revalidation/ui/actions.js b/exercises/05.actions/04.solution.revalidation/ui/actions.js new file mode 100644 index 0000000..71008e9 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/ui/actions.js @@ -0,0 +1,15 @@ +'use server' + +import * as db from '../db/ship-api.js' + +export async function updateShipName(previousState, formData) { + try { + await db.updateShipName({ + shipId: formData.get('shipId'), + shipName: formData.get('shipName'), + }) + return { status: 'success', message: 'Success!' } + } catch (error) { + return { status: 'error', message: error?.message || String(error) } + } +} diff --git a/exercises/05.actions/04.solution.revalidation/ui/app.js b/exercises/05.actions/04.solution.revalidation/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/05.actions/04.solution.revalidation/ui/content-cache.js b/exercises/05.actions/04.solution.revalidation/ui/content-cache.js new file mode 100644 index 0000000..612b97f --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/ui/content-cache.js @@ -0,0 +1,46 @@ +import { useSyncExternalStore } from 'react' + +/** + * This will call the given callback function whenever the contents of the map + * change. + */ +class ObservableMap extends Map { + listeners = new Set() + set(key, value) { + const result = super.set(key, value) + this.emitChange() + return result + } + delete(key) { + const result = super.delete(key) + this.emitChange() + return result + } + emitChange() { + for (const listener of this.listeners) { + listener() + } + } + subscribe(listener) { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } +} + +export const contentCache = new ObservableMap() + +export function generateKey() { + return Date.now().toString(36) + Math.random().toString(36).slice(2) +} + +export function useContentCache() { + function subscribe(cb) { + return contentCache.subscribe(cb) + } + function getSnapshot() { + return contentCache + } + return useSyncExternalStore(subscribe, getSnapshot) +} diff --git a/exercises/05.actions/04.solution.revalidation/ui/edit-text.js b/exercises/05.actions/04.solution.revalidation/ui/edit-text.js new file mode 100644 index 0000000..1904aad --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/ui/edit-text.js @@ -0,0 +1,105 @@ +'use client' + +import { createElement as h, useRef, useState, useActionState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, action, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const [formState, formAction, isPending] = useActionState(action) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + { style: { opacity: isPending ? 0.6 : 1 } }, + edit + ? h( + 'form', + { + action: formAction, + onSubmit: () => { + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + h( + 'div', + { position: 'relative' }, + formState + ? h( + 'div', + { + style: { + position: 'absolute', + left: 0, + right: 0, + color: formState.status === 'error' ? 'red' : 'green', + fontSize: '0.75rem', + fontWeight: 'normal', + }, + }, + formState.message, + ) + : null, + ), + ) +} diff --git a/exercises/05.actions/04.solution.revalidation/ui/error-boundary.js b/exercises/05.actions/04.solution.revalidation/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/05.actions/04.solution.revalidation/ui/img-utils.js b/exercises/05.actions/04.solution.revalidation/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/05.actions/04.solution.revalidation/ui/img.js b/exercises/05.actions/04.solution.revalidation/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/05.actions/04.solution.revalidation/ui/index.js b/exercises/05.actions/04.solution.revalidation/ui/index.js new file mode 100644 index 0000000..411988a --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/ui/index.js @@ -0,0 +1,172 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useEffect, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { contentCache, useContentCache, generateKey } from './content-cache.js' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function updateContentKey() { + console.error('updateContentKey called before it was set!') +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + callServer, + }) +} + +async function callServer(id, args) { + const fetchPromise = fetch(`/action${getGlobalLocation()}`, { + method: 'POST', + headers: { 'rsc-action': id }, + body: await RSC.encodeReply(args), + }) + const contentKey = window.history.state?.key ?? generateKey() + onStreamFinished(fetchPromise, () => { + updateContentKey(contentKey) + }) + const actionResponsePromise = createFromFetch(fetchPromise) + contentCache.set(contentKey, actionResponsePromise) + const { returnValue } = await actionResponsePromise + return returnValue +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +let initialContentKey = window.history.state?.key +if (!initialContentKey) { + initialContentKey = generateKey() + window.history.replaceState({ key: initialContentKey }, '') +} +contentCache.set(initialContentKey, initialContentPromise) + +function onStreamFinished(fetchPromise, onFinished) { + // create a promise chain that resolves when the stream is completely consumed + return ( + fetchPromise + // clone the response so createFromFetch can use it (otherwise we lock the reader) + // and wait for the text to be consumed so we know the stream is finished + .then((response) => response.clone().text()) + .then(onFinished) + ) +} + +function Root() { + const latestNav = useRef(null) + const contentCache = useContentCache() + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentKey, setContentKey] = useState(initialContentKey) + const [isPending, startTransition] = useTransition() + + // set the updateContentKey function in a useEffect to avoid issues with + // concurrent rendering (useDeferredValue will create throw-away renders). + useEffect(() => { + updateContentKey = (newContentKey) => { + startTransition(() => setContentKey(newContentKey)) + } + }, []) + + const location = useDeferredValue(nextLocation) + const contentPromise = contentCache.get(contentKey) + + useEffect(() => { + function handlePopState() { + const nextLocation = getGlobalLocation() + setNextLocation(nextLocation) + const historyKey = window.history.state?.key ?? generateKey() + + if (!contentCache.has(historyKey)) { + const fetchPromise = fetchContent(nextLocation) + const nextContentPromise = createFromFetch(fetchPromise) + contentCache.set(historyKey, nextContentPromise) + } + + updateContentKey(historyKey) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, [contentCache]) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + const newContentKey = generateKey() + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + window.history.replaceState({ key: newContentKey }, '', nextLocation) + } else { + window.history.pushState({ key: newContentKey }, '', nextLocation) + } + return response + }), + ) + + contentCache.set(newContentKey, nextContentPromise) + updateContentKey(newContentKey) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise).root, + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/05.actions/04.solution.revalidation/ui/router.js b/exercises/05.actions/04.solution.revalidation/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/05.actions/04.solution.revalidation/ui/ship-details-pending.js b/exercises/05.actions/04.solution.revalidation/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/05.actions/04.solution.revalidation/ui/ship-details.js b/exercises/05.actions/04.solution.revalidation/ui/ship-details.js new file mode 100644 index 0000000..cd702fa --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/ui/ship-details.js @@ -0,0 +1,115 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { updateShipName } from './actions.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + action: updateShipName, + initialValue: 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/05.actions/04.solution.revalidation/ui/ship-search-results.js b/exercises/05.actions/04.solution.revalidation/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/05.actions/04.solution.revalidation/ui/ship-search.js b/exercises/05.actions/04.solution.revalidation/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/05.actions/04.solution.revalidation/ui/spin-delay.js b/exercises/05.actions/04.solution.revalidation/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/05.actions/04.solution.revalidation/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/05.actions/05.problem.history-revalidation/.gitignore b/exercises/05.actions/05.problem.history-revalidation/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/05.actions/05.problem.history-revalidation/README.mdx b/exercises/05.actions/05.problem.history-revalidation/README.mdx new file mode 100644 index 0000000..393cf04 --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/README.mdx @@ -0,0 +1,15 @@ +# History Revalidation + + + +๐Ÿ‘จโ€๐Ÿ’ผ You might notice that now if you make a change to one of the ships, then hit +the back button the change will be lost. This is because the history is not +being revalidated when the back button is clicked. + +Interestingly, this is the way the web platform works by default. When you +navigate back to a page, the browser will use a cached version of that page and +display it. + +But we can do better. Let's revalidate on `popstate` events as well! + +This will be similar to the revalidation we did earlier. Good luck! diff --git a/exercises/05.actions/05.problem.history-revalidation/db/ship-api.js b/exercises/05.actions/05.problem.history-revalidation/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/05.actions/05.problem.history-revalidation/db/ships.json b/exercises/05.actions/05.problem.history-revalidation/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/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/05.actions/05.problem.history-revalidation/package.json b/exercises/05.actions/05.problem.history-revalidation/package.json new file mode 100644 index 0000000..093bd3c --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__05.actions__sep__05.problem.history-revalidation", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/05.actions/05.problem.history-revalidation/public/favicon.ico b/exercises/05.actions/05.problem.history-revalidation/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/favicon.ico differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/favicon.svg b/exercises/05.actions/05.problem.history-revalidation/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/05.actions/05.problem.history-revalidation/public/iframe-sync.js b/exercises/05.actions/05.problem.history-revalidation/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/broken-ship.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/broken-ship.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/fallback-ship.png b/exercises/05.actions/05.problem.history-revalidation/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/fallback-ship.png differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/0268fc4817ad1.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/1ae7b4b92036b.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/1ff1991efe029.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/3ba8aa65ffe6c.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/441f7092a8d44.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/5c13d8b28a14a.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/627c497212456.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/627c497212456.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/670003aed3795.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/670003aed3795.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/6c86fca8b9086.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/6f375578ead88.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/ab267a5984523.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/b442531ea32b2.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/bc4cbadf89bd3.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/cb03cc4e5717e.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/cfd10fcd2de6c.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/d3b8aa65ffe6c.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/d486d48b82b81.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/e92cefe4f6727.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/ec7a3f950f99f.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/f3d9a88e1c234.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/img/ships/fdc13cb488bf1.webp b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/05.actions/05.problem.history-revalidation/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/05.actions/05.problem.history-revalidation/public/index.html b/exercises/05.actions/05.problem.history-revalidation/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/05.actions/05.problem.history-revalidation/public/style.css b/exercises/05.actions/05.problem.history-revalidation/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/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/05.actions/05.problem.history-revalidation/server/app.js b/exercises/05.actions/05.problem.history-revalidation/server/app.js new file mode 100644 index 0000000..f26cb41 --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/server/app.js @@ -0,0 +1,103 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { + renderToPipeableStream, + decodeReply, +} from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +const moduleBasePath = new URL('../ui', import.meta.url).href + +async function renderApp(context, returnValue) { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const root = h(App) + const payload = { root, returnValue } + const { pipe } = renderToPipeableStream(payload, moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +} + +app.get('/rsc/:shipId?', async (context) => renderApp(context, null)) + +app.post('/action/:shipId?', async (context) => { + const serverReference = context.req.header('rsc-action') + const [filepath, name] = serverReference.split('#') + const action = (await import(filepath))[name] + // Validate that this is actually a function we intended to expose and + // not the client trying to invoke arbitrary functions. In a real app, + // you'd have a manifest verifying this before even importing it. + if (action.$$typeof !== Symbol.for('react.server.reference')) { + throw new Error('Invalid action') + } + + const formData = await context.req.formData() + const args = await decodeReply(formData, moduleBasePath) + const result = await action(...args) + return await renderApp(context, result) +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/05.actions/05.problem.history-revalidation/server/async-storage.js b/exercises/05.actions/05.problem.history-revalidation/server/async-storage.js new file mode 100644 index 0000000..b9a08b1 --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/server/async-storage.js @@ -0,0 +1,3 @@ +import { AsyncLocalStorage } from 'node:async_hooks' + +export const shipDataStorage = new AsyncLocalStorage() diff --git a/exercises/05.actions/05.problem.history-revalidation/server/register-rsc-loader.js b/exercises/05.actions/05.problem.history-revalidation/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/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/05.actions/05.problem.history-revalidation/server/rsc-loader.js b/exercises/05.actions/05.problem.history-revalidation/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/05.actions/05.problem.history-revalidation/tests/history-revalidation.test.js b/exercises/05.actions/05.problem.history-revalidation/tests/history-revalidation.test.js new file mode 100644 index 0000000..f9b7dd0 --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/tests/history-revalidation.test.js @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test' +import { searchShips } from '../db/ship-api.js' + +test('Submitting the form posts to the action endpoint correctly', async ({ + page, +}) => { + const { + ships: [ship], + } = await searchShips({ search: '' }) + await page.goto(`/`) + await page.waitForLoadState('networkidle') + + await page.getByRole('link', { name: ship.name }).click() + + await page.getByRole('button', { name: ship.name }).click() + + const newName = `${ship.name} ${Math.random().toString(16).slice(2, 5)}` + + // Change the value of the input + await page.getByRole('textbox', { name: 'Ship Name' }).fill(newName) + + // Press Enter + await page.keyboard.press('Enter') + + // Verify the name of the ship in the list has been updated + await expect(await page.getByRole('list').getByText(newName)).toBeVisible() + + await page.goBack() + + await expect( + await page.getByRole('list').getByText(newName), + '๐Ÿšจ the history cache was not revalidated. Make sure to revalidate the cache during the popstate event.', + ).toBeVisible() +}) diff --git a/exercises/05.actions/05.problem.history-revalidation/tests/playwright.config.js b/exercises/05.actions/05.problem.history-revalidation/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/05.actions/05.problem.history-revalidation/tests/smoke.test.js b/exercises/05.actions/05.problem.history-revalidation/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/05.actions/05.problem.history-revalidation/ui/actions.js b/exercises/05.actions/05.problem.history-revalidation/ui/actions.js new file mode 100644 index 0000000..71008e9 --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/ui/actions.js @@ -0,0 +1,15 @@ +'use server' + +import * as db from '../db/ship-api.js' + +export async function updateShipName(previousState, formData) { + try { + await db.updateShipName({ + shipId: formData.get('shipId'), + shipName: formData.get('shipName'), + }) + return { status: 'success', message: 'Success!' } + } catch (error) { + return { status: 'error', message: error?.message || String(error) } + } +} diff --git a/exercises/05.actions/05.problem.history-revalidation/ui/app.js b/exercises/05.actions/05.problem.history-revalidation/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/05.actions/05.problem.history-revalidation/ui/content-cache.js b/exercises/05.actions/05.problem.history-revalidation/ui/content-cache.js new file mode 100644 index 0000000..612b97f --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/ui/content-cache.js @@ -0,0 +1,46 @@ +import { useSyncExternalStore } from 'react' + +/** + * This will call the given callback function whenever the contents of the map + * change. + */ +class ObservableMap extends Map { + listeners = new Set() + set(key, value) { + const result = super.set(key, value) + this.emitChange() + return result + } + delete(key) { + const result = super.delete(key) + this.emitChange() + return result + } + emitChange() { + for (const listener of this.listeners) { + listener() + } + } + subscribe(listener) { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } +} + +export const contentCache = new ObservableMap() + +export function generateKey() { + return Date.now().toString(36) + Math.random().toString(36).slice(2) +} + +export function useContentCache() { + function subscribe(cb) { + return contentCache.subscribe(cb) + } + function getSnapshot() { + return contentCache + } + return useSyncExternalStore(subscribe, getSnapshot) +} diff --git a/exercises/05.actions/05.problem.history-revalidation/ui/edit-text.js b/exercises/05.actions/05.problem.history-revalidation/ui/edit-text.js new file mode 100644 index 0000000..1904aad --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/ui/edit-text.js @@ -0,0 +1,105 @@ +'use client' + +import { createElement as h, useRef, useState, useActionState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, action, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const [formState, formAction, isPending] = useActionState(action) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + { style: { opacity: isPending ? 0.6 : 1 } }, + edit + ? h( + 'form', + { + action: formAction, + onSubmit: () => { + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + h( + 'div', + { position: 'relative' }, + formState + ? h( + 'div', + { + style: { + position: 'absolute', + left: 0, + right: 0, + color: formState.status === 'error' ? 'red' : 'green', + fontSize: '0.75rem', + fontWeight: 'normal', + }, + }, + formState.message, + ) + : null, + ), + ) +} diff --git a/exercises/05.actions/05.problem.history-revalidation/ui/error-boundary.js b/exercises/05.actions/05.problem.history-revalidation/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/05.actions/05.problem.history-revalidation/ui/img-utils.js b/exercises/05.actions/05.problem.history-revalidation/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/05.actions/05.problem.history-revalidation/ui/img.js b/exercises/05.actions/05.problem.history-revalidation/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/05.actions/05.problem.history-revalidation/ui/index.js b/exercises/05.actions/05.problem.history-revalidation/ui/index.js new file mode 100644 index 0000000..4b5673a --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/ui/index.js @@ -0,0 +1,181 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useEffect, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { contentCache, useContentCache, generateKey } from './content-cache.js' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function updateContentKey() { + console.error('updateContentKey called before it was set!') +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + callServer, + }) +} + +async function callServer(id, args) { + const fetchPromise = fetch(`/action${getGlobalLocation()}`, { + method: 'POST', + headers: { 'rsc-action': id }, + body: await RSC.encodeReply(args), + }) + const contentKey = window.history.state?.key ?? generateKey() + onStreamFinished(fetchPromise, () => { + updateContentKey(contentKey) + }) + const actionResponsePromise = createFromFetch(fetchPromise) + contentCache.set(contentKey, actionResponsePromise) + const { returnValue } = await actionResponsePromise + return returnValue +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +let initialContentKey = window.history.state?.key +if (!initialContentKey) { + initialContentKey = generateKey() + window.history.replaceState({ key: initialContentKey }, '') +} +contentCache.set(initialContentKey, initialContentPromise) + +function onStreamFinished(fetchPromise, onFinished) { + // create a promise chain that resolves when the stream is completely consumed + return ( + fetchPromise + // clone the response so createFromFetch can use it (otherwise we lock the reader) + // and wait for the text to be consumed so we know the stream is finished + .then((response) => response.clone().text()) + .then(onFinished) + ) +} + +function Root() { + const latestNav = useRef(null) + const contentCache = useContentCache() + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentKey, setContentKey] = useState(initialContentKey) + const [isPending, startTransition] = useTransition() + + // set the updateContentKey function in a useEffect to avoid issues with + // concurrent rendering (useDeferredValue will create throw-away renders). + useEffect(() => { + updateContentKey = (newContentKey) => { + startTransition(() => setContentKey(newContentKey)) + } + }, []) + + const location = useDeferredValue(nextLocation) + const contentPromise = contentCache.get(contentKey) + + useEffect(() => { + function handlePopState() { + const nextLocation = getGlobalLocation() + setNextLocation(nextLocation) + const historyKey = window.history.state?.key ?? generateKey() + + // ๐Ÿจ declare "let nextContentPromise" here + // ๐Ÿจ move the fetchPromise up from the if statement below because now we're going to revalidate all the time + // ๐Ÿจ when the fetchPromise stream is finished (๐Ÿ’ฐ onStreamFinished): + // set the historyKey in the contentCache to nextContentPromise in a startTransition + // ๐Ÿจ assign nextContentPromise to createFromFetch(fetchPromise) + + if (!contentCache.has(historyKey)) { + // ๐Ÿจ move these two things up because we're going to do it all the time: + const fetchPromise = fetchContent(nextLocation) + const nextContentPromise = createFromFetch(fetchPromise) + + // if we don't have this key in the cache already, set it now + contentCache.set(historyKey, nextContentPromise) + } + + updateContentKey(historyKey) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, [contentCache]) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + const newContentKey = generateKey() + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + window.history.replaceState({ key: newContentKey }, '', nextLocation) + } else { + window.history.pushState({ key: newContentKey }, '', nextLocation) + } + return response + }), + ) + + contentCache.set(newContentKey, nextContentPromise) + updateContentKey(newContentKey) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise).root, + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/05.actions/05.problem.history-revalidation/ui/router.js b/exercises/05.actions/05.problem.history-revalidation/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/05.actions/05.problem.history-revalidation/ui/ship-details-pending.js b/exercises/05.actions/05.problem.history-revalidation/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/05.actions/05.problem.history-revalidation/ui/ship-details.js b/exercises/05.actions/05.problem.history-revalidation/ui/ship-details.js new file mode 100644 index 0000000..cd702fa --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/ui/ship-details.js @@ -0,0 +1,115 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { updateShipName } from './actions.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + action: updateShipName, + initialValue: 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/05.actions/05.problem.history-revalidation/ui/ship-search-results.js b/exercises/05.actions/05.problem.history-revalidation/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/05.actions/05.problem.history-revalidation/ui/ship-search.js b/exercises/05.actions/05.problem.history-revalidation/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/05.actions/05.problem.history-revalidation/ui/spin-delay.js b/exercises/05.actions/05.problem.history-revalidation/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/05.actions/05.problem.history-revalidation/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/05.actions/05.solution.history-revalidation/.gitignore b/exercises/05.actions/05.solution.history-revalidation/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/exercises/05.actions/05.solution.history-revalidation/README.mdx b/exercises/05.actions/05.solution.history-revalidation/README.mdx new file mode 100644 index 0000000..4534690 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/README.mdx @@ -0,0 +1,10 @@ +# History Revalidation + + + +๐Ÿ‘จโ€๐Ÿ’ผ Stellar! You should be proud of yourself for making such a great user +experience when the user hits forward and back. + +You might be bothered by the momentary flash of the old state when you hit back +while that state is getting revalidated. This is a trade-off of being able to +navigate the user immediately. diff --git a/exercises/05.actions/05.solution.history-revalidation/db/ship-api.js b/exercises/05.actions/05.solution.history-revalidation/db/ship-api.js new file mode 100644 index 0000000..36e89c7 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/db/ship-api.js @@ -0,0 +1,59 @@ +import fs from 'node:fs/promises' + +const shipData = JSON.parse( + String(await fs.readFile(new URL('./ships.json', import.meta.url))), +) + +const MIN_DELAY = 200 +const MAX_DELAY = 500 + +export async function searchShips({ + search, + delay = Math.random() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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() * (MAX_DELAY - MIN_DELAY) + MIN_DELAY, +}) { + 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}"`) + } + if (shipName.toLowerCase().includes('error')) { + throw new Error('Error updating ship name') + } + if (shipName === ship.name) { + throw new Error('New name is the same as the old name') + } + ship.name = shipName + return ship +} diff --git a/exercises/05.actions/05.solution.history-revalidation/db/ships.json b/exercises/05.actions/05.solution.history-revalidation/db/ships.json new file mode 100644 index 0000000..271493e --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/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/05.actions/05.solution.history-revalidation/package.json b/exercises/05.actions/05.solution.history-revalidation/package.json new file mode 100644 index 0000000..499f984 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/package.json @@ -0,0 +1,25 @@ +{ + "name": "exercises__sep__05.actions__sep__05.solution.history-revalidation", + "version": "1.0.0", + "type": "module", + "private": true, + "description": "Super simple implementation of RSCs with minimal deps", + "main": "index.js", + "scripts": { + "dev": "node --import ./server/register-rsc-loader.js --conditions=react-server --watch server/app.js", + "test": "playwright test --config=./tests/playwright.config.js" + }, + "keywords": [], + "author": "Kent C. Dodds (https://kentcdodds.com/)", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" + } +} diff --git a/exercises/05.actions/05.solution.history-revalidation/public/favicon.ico b/exercises/05.actions/05.solution.history-revalidation/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/favicon.ico differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/favicon.svg b/exercises/05.actions/05.solution.history-revalidation/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/exercises/05.actions/05.solution.history-revalidation/public/iframe-sync.js b/exercises/05.actions/05.solution.history-revalidation/public/iframe-sync.js new file mode 100644 index 0000000..b9c653c --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/public/iframe-sync.js @@ -0,0 +1,41 @@ +// this is for the epic workshop application integration. If you're looking at +// the app from the iframe in the workshop app, there's a URL bar in the app +// that needs to know what the current URL in the child app is, so this bit of +// code keeps them in sync. + +if (window.parent !== window) { + window.parent.postMessage( + { type: 'epicshop:loaded', url: window.location.href }, + '*', + ) + function handleMessage(event) { + const { type, params } = event.data + if (type === 'epicshop:navigate-call') { + const [distanceOrUrl, options] = params + if (typeof distanceOrUrl === 'number') { + window.history.go(distanceOrUrl) + } else { + if (options?.replace) { + window.location.replace(distanceOrUrl) + } else { + window.location.assign(distanceOrUrl) + } + } + } + } + + window.addEventListener('message', handleMessage) + + const methods = ['pushState', 'replaceState', 'go', 'forward', 'back'] + for (const method of methods) { + window.history[method] = new Proxy(window.history[method], { + apply(target, thisArg, argArray) { + window.parent.postMessage( + { type: 'epicshop:history-call', method, args: argArray }, + '*', + ) + return target.apply(thisArg, argArray) + }, + }) + } +} diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/broken-ship.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/broken-ship.webp new file mode 100644 index 0000000..1224fef Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/broken-ship.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/fallback-ship.png b/exercises/05.actions/05.solution.history-revalidation/public/img/fallback-ship.png new file mode 100644 index 0000000..9622c4b Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/fallback-ship.png differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/0268fc4817ad1.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/0268fc4817ad1.webp new file mode 100644 index 0000000..2238343 Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/0268fc4817ad1.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/1ae7b4b92036b.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/1ae7b4b92036b.webp new file mode 100644 index 0000000..410f86d Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/1ae7b4b92036b.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/1ff1991efe029.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/1ff1991efe029.webp new file mode 100644 index 0000000..a0de0c3 Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/1ff1991efe029.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/3ba8aa65ffe6c.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/3ba8aa65ffe6c.webp new file mode 100644 index 0000000..32fdb3d Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/3ba8aa65ffe6c.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/441f7092a8d44.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/441f7092a8d44.webp new file mode 100644 index 0000000..658aad9 Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/441f7092a8d44.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/5c13d8b28a14a.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/5c13d8b28a14a.webp new file mode 100644 index 0000000..179ef6b Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/5c13d8b28a14a.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/627c497212456.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/627c497212456.webp new file mode 100644 index 0000000..2ded762 Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/627c497212456.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/670003aed3795.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/670003aed3795.webp new file mode 100644 index 0000000..4cb1a25 Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/670003aed3795.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/6c86fca8b9086.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/6c86fca8b9086.webp new file mode 100644 index 0000000..972f811 Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/6c86fca8b9086.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/6f375578ead88.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/6f375578ead88.webp new file mode 100644 index 0000000..37b9c8b Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/6f375578ead88.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/ab267a5984523.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/ab267a5984523.webp new file mode 100644 index 0000000..bf96b34 Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/ab267a5984523.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/b442531ea32b2.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/b442531ea32b2.webp new file mode 100644 index 0000000..dde2e65 Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/b442531ea32b2.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/bc4cbadf89bd3.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/bc4cbadf89bd3.webp new file mode 100644 index 0000000..d920ab4 Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/bc4cbadf89bd3.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/cb03cc4e5717e.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/cb03cc4e5717e.webp new file mode 100644 index 0000000..e79d50c Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/cb03cc4e5717e.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/cfd10fcd2de6c.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/cfd10fcd2de6c.webp new file mode 100644 index 0000000..79521fe Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/cfd10fcd2de6c.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/d3b8aa65ffe6c.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/d3b8aa65ffe6c.webp new file mode 100644 index 0000000..05fb624 Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/d3b8aa65ffe6c.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/d486d48b82b81.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/d486d48b82b81.webp new file mode 100644 index 0000000..4933602 Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/d486d48b82b81.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/e92cefe4f6727.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/e92cefe4f6727.webp new file mode 100644 index 0000000..377ecba Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/e92cefe4f6727.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/ec7a3f950f99f.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/ec7a3f950f99f.webp new file mode 100644 index 0000000..3457096 Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/ec7a3f950f99f.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/f3d9a88e1c234.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/f3d9a88e1c234.webp new file mode 100644 index 0000000..216814c Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/f3d9a88e1c234.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/img/ships/fdc13cb488bf1.webp b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/fdc13cb488bf1.webp new file mode 100644 index 0000000..fdebeb7 Binary files /dev/null and b/exercises/05.actions/05.solution.history-revalidation/public/img/ships/fdc13cb488bf1.webp differ diff --git a/exercises/05.actions/05.solution.history-revalidation/public/index.html b/exercises/05.actions/05.solution.history-revalidation/public/index.html new file mode 100644 index 0000000..176657e --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/public/index.html @@ -0,0 +1,27 @@ + + + + + + + + Starship Deets + + + + +
+ + + + diff --git a/exercises/05.actions/05.solution.history-revalidation/public/style.css b/exercises/05.actions/05.solution.history-revalidation/public/style.css new file mode 100644 index 0000000..1186ebb --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/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/05.actions/05.solution.history-revalidation/server/app.js b/exercises/05.actions/05.solution.history-revalidation/server/app.js new file mode 100644 index 0000000..43d4929 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/server/app.js @@ -0,0 +1,104 @@ +import { readFile } from 'fs/promises' +import { serve } from '@hono/node-server' +import { serveStatic } from '@hono/node-server/serve-static' +import { RESPONSE_ALREADY_SENT } from '@hono/node-server/utils/response' +import closeWithGrace from 'close-with-grace' +import { Hono } from 'hono' +import { trimTrailingSlash } from 'hono/trailing-slash' +import { createElement as h } from 'react' +import { + renderToPipeableStream, + decodeReply, +} from 'react-server-dom-esm/server' +import { App } from '../ui/app.js' +import { shipDataStorage } from './async-storage.js' + +const PORT = process.env.PORT || 3000 + +const app = new Hono({ strict: true }) + +app.use(trimTrailingSlash()) + +app.use('/*', serveStatic({ root: './public', index: '' })) + +app.use( + '/ui/*', + serveStatic({ + root: './ui', + onNotFound: (path, context) => context.text('File not found', 404), + rewriteRequestPath: (path) => path.replace('/ui', ''), + }), +) + +// This just cleans up the URL if the search ever gets cleared... Not important +// for RSCs... Just ... I just can't help myself. I like URLs clean. +app.use(async (context, next) => { + if (context.req.query('search') === '') { + const url = new URL(context.req.url) + const searchParams = new URLSearchParams(url.search) + searchParams.delete('search') + const location = [url.pathname, searchParams.toString()] + .filter(Boolean) + .join('?') + return context.redirect(location, 302) + } else { + await next() + } +}) + +const moduleBasePath = new URL('../ui', import.meta.url).href + +async function renderApp(context, returnValue) { + const shipId = context.req.param('shipId') || null + const search = context.req.query('search') || '' + const data = { shipId, search } + shipDataStorage.run(data, () => { + const root = h(App) + const payload = { root, returnValue } + const { pipe } = renderToPipeableStream(payload, moduleBasePath) + pipe(context.env.outgoing) + }) + return RESPONSE_ALREADY_SENT +} + +app.get('/rsc/:shipId?', async (context) => renderApp(context, null)) + +app.post('/action/:shipId?', async (context) => { + const serverReference = context.req.header('rsc-action') + const [filepath, name] = serverReference.split('#') + const action = (await import(filepath))[name] + // Validate that this is actually a function we intended to expose and + // not the client trying to invoke arbitrary functions. In a real app, + // you'd have a manifest verifying this before even importing it. + if (action.$$typeof !== Symbol.for('react.server.reference')) { + throw new Error('Invalid action') + } + + const formData = await context.req.formData() + const args = await decodeReply(formData, moduleBasePath) + const result = await action(...args) + return await renderApp(context, result) +}) + +app.get('/:shipId?', async (context) => { + const html = await readFile('./public/index.html', 'utf8') + return context.html(html, 200) +}) + +app.onError((err, context) => { + console.error('error', err) + return context.json({ error: true, message: 'Something went wrong' }, 500) +}) + +const server = serve({ fetch: app.fetch, port: PORT }, (info) => { + const url = `http://localhost:${info.port}` + console.log(`๐Ÿš€ We have liftoff!\n${url}`) +}) + +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) => server.close(resolve)) + process.exit() +}) diff --git a/exercises/05.actions/05.solution.history-revalidation/server/async-storage.js b/exercises/05.actions/05.solution.history-revalidation/server/async-storage.js new file mode 100644 index 0000000..b9a08b1 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/server/async-storage.js @@ -0,0 +1,3 @@ +import { AsyncLocalStorage } from 'node:async_hooks' + +export const shipDataStorage = new AsyncLocalStorage() diff --git a/exercises/05.actions/05.solution.history-revalidation/server/register-rsc-loader.js b/exercises/05.actions/05.solution.history-revalidation/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/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/05.actions/05.solution.history-revalidation/server/rsc-loader.js b/exercises/05.actions/05.solution.history-revalidation/server/rsc-loader.js new file mode 100644 index 0000000..875f1b2 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/server/rsc-loader.js @@ -0,0 +1,24 @@ +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) { + const result = await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) + return result +} diff --git a/exercises/05.actions/05.solution.history-revalidation/tests/history-revalidation.test.js b/exercises/05.actions/05.solution.history-revalidation/tests/history-revalidation.test.js new file mode 100644 index 0000000..f9b7dd0 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/tests/history-revalidation.test.js @@ -0,0 +1,34 @@ +import { test, expect } from '@playwright/test' +import { searchShips } from '../db/ship-api.js' + +test('Submitting the form posts to the action endpoint correctly', async ({ + page, +}) => { + const { + ships: [ship], + } = await searchShips({ search: '' }) + await page.goto(`/`) + await page.waitForLoadState('networkidle') + + await page.getByRole('link', { name: ship.name }).click() + + await page.getByRole('button', { name: ship.name }).click() + + const newName = `${ship.name} ${Math.random().toString(16).slice(2, 5)}` + + // Change the value of the input + await page.getByRole('textbox', { name: 'Ship Name' }).fill(newName) + + // Press Enter + await page.keyboard.press('Enter') + + // Verify the name of the ship in the list has been updated + await expect(await page.getByRole('list').getByText(newName)).toBeVisible() + + await page.goBack() + + await expect( + await page.getByRole('list').getByText(newName), + '๐Ÿšจ the history cache was not revalidated. Make sure to revalidate the cache during the popstate event.', + ).toBeVisible() +}) diff --git a/exercises/05.actions/05.solution.history-revalidation/tests/playwright.config.js b/exercises/05.actions/05.solution.history-revalidation/tests/playwright.config.js new file mode 100644 index 0000000..b7bd9f2 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/tests/playwright.config.js @@ -0,0 +1,41 @@ +import os from 'os' +import path from 'path' +import { defineConfig, devices } from '@playwright/test' + +const PORT = process.env.PORT || '3000' + +const tmpDir = path.join(os.tmpdir(), 'epicreact-server-components') + +export default defineConfig({ + timeout: 10000 * (process.env.CI ? 10 : 1), + expect: { + timeout: 5000 * (process.env.CI ? 10 : 1), + }, + outputDir: path.join(tmpDir, 'playwright-test-output'), + reporter: [ + [ + 'html', + { open: 'never', outputFolder: path.join(tmpDir, 'playwright-report') }, + ], + ], + use: { + baseURL: `http://localhost:${PORT}/`, + trace: 'retain-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + webServer: { + command: 'npm run dev', + port: Number(PORT), + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + env: { PORT }, + }, +}) diff --git a/exercises/05.actions/05.solution.history-revalidation/tests/smoke.test.js b/exercises/05.actions/05.solution.history-revalidation/tests/smoke.test.js new file mode 100644 index 0000000..b29d44b --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/tests/smoke.test.js @@ -0,0 +1,44 @@ +import { test, expect } from '@playwright/test' + +test('should display the home page and perform search', async ({ page }) => { + await page.goto('/') + await page.waitForLoadState('networkidle') + await expect(page).toHaveTitle('Starship Deets') + + // Check for the filter input + const filterInput = page.getByPlaceholder('filter ships') + await expect(filterInput).toBeVisible() + + // Perform a search + await filterInput.fill('hopper') + + // Verify URL change with search params + await expect(page).toHaveURL('/?search=hopper') + + const shipList = page.getByRole('list').first() + + // Wait for the list to be filtered down to two items + await expect(shipList.getByRole('listitem')).toHaveCount(2) + + // Verify filtered results + const shipLinks = page + .getByRole('list') + .first() + .getByRole('listitem') + .getByRole('link') + for (const link of await shipLinks.all()) { + await expect(link).toContainText('hopper', { ignoreCase: true }) + } + + // Find and click on a ship in the filtered list + const shipLink = shipLinks.first() + const shipName = await shipLink.textContent() + await shipLink.click() + + // Verify URL change + await expect(page).toHaveURL(/\/[a-zA-Z0-9-]+/) + + // Verify ship detail view + const shipTitle = page.getByRole('heading', { level: 2 }) + await expect(shipTitle).toHaveText(shipName) +}) diff --git a/exercises/05.actions/05.solution.history-revalidation/ui/actions.js b/exercises/05.actions/05.solution.history-revalidation/ui/actions.js new file mode 100644 index 0000000..71008e9 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/ui/actions.js @@ -0,0 +1,15 @@ +'use server' + +import * as db from '../db/ship-api.js' + +export async function updateShipName(previousState, formData) { + try { + await db.updateShipName({ + shipId: formData.get('shipId'), + shipName: formData.get('shipName'), + }) + return { status: 'success', message: 'Success!' } + } catch (error) { + return { status: 'error', message: error?.message || String(error) } + } +} diff --git a/exercises/05.actions/05.solution.history-revalidation/ui/app.js b/exercises/05.actions/05.solution.history-revalidation/ui/app.js new file mode 100644 index 0000000..60ddd4f --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/ui/app.js @@ -0,0 +1,35 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.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 function App() { + const { shipId, search } = shipDataStorage.getStore() + return h( + 'div', + { className: 'app' }, + 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/05.actions/05.solution.history-revalidation/ui/content-cache.js b/exercises/05.actions/05.solution.history-revalidation/ui/content-cache.js new file mode 100644 index 0000000..612b97f --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/ui/content-cache.js @@ -0,0 +1,46 @@ +import { useSyncExternalStore } from 'react' + +/** + * This will call the given callback function whenever the contents of the map + * change. + */ +class ObservableMap extends Map { + listeners = new Set() + set(key, value) { + const result = super.set(key, value) + this.emitChange() + return result + } + delete(key) { + const result = super.delete(key) + this.emitChange() + return result + } + emitChange() { + for (const listener of this.listeners) { + listener() + } + } + subscribe(listener) { + this.listeners.add(listener) + return () => { + this.listeners.delete(listener) + } + } +} + +export const contentCache = new ObservableMap() + +export function generateKey() { + return Date.now().toString(36) + Math.random().toString(36).slice(2) +} + +export function useContentCache() { + function subscribe(cb) { + return contentCache.subscribe(cb) + } + function getSnapshot() { + return contentCache + } + return useSyncExternalStore(subscribe, getSnapshot) +} diff --git a/exercises/05.actions/05.solution.history-revalidation/ui/edit-text.js b/exercises/05.actions/05.solution.history-revalidation/ui/edit-text.js new file mode 100644 index 0000000..1904aad --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/ui/edit-text.js @@ -0,0 +1,105 @@ +'use client' + +import { createElement as h, useRef, useState, useActionState } from 'react' +import { flushSync } from 'react-dom' + +const inheritStyles = { + fontSize: 'inherit', + fontStyle: 'inherit', + fontWeight: 'inherit', + fontFamily: 'inherit', + textAlign: 'inherit', +} + +export function EditableText({ id, shipId, action, initialValue = '' }) { + const [edit, setEdit] = useState(false) + const [value, setValue] = useState(initialValue) + const [formState, formAction, isPending] = useActionState(action) + const inputRef = useRef(null) + const buttonRef = useRef(null) + return h( + 'div', + { style: { opacity: isPending ? 0.6 : 1 } }, + edit + ? h( + 'form', + { + action: formAction, + onSubmit: () => { + setValue(inputRef.current?.value ?? '') + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + }, + }, + h('input', { + type: 'hidden', + name: 'shipId', + value: shipId, + }), + h('input', { + required: true, + ref: inputRef, + type: 'text', + id: id, + 'aria-label': 'Ship Name', + name: 'shipName', + defaultValue: value, + style: { + border: 'none', + background: 'none', + width: '100%', + ...inheritStyles, + }, + onKeyDown: (event) => { + if (event.key === 'Escape') { + flushSync(() => { + setEdit(false) + }) + buttonRef.current?.focus() + } + }, + }), + ) + : h( + 'button', + { + ref: buttonRef, + type: 'button', + style: { + border: 'none', + background: 'none', + ...inheritStyles, + }, + onClick: () => { + flushSync(() => { + setEdit(true) + }) + inputRef.current?.select() + }, + }, + value || 'Edit', + ), + h( + 'div', + { position: 'relative' }, + formState + ? h( + 'div', + { + style: { + position: 'absolute', + left: 0, + right: 0, + color: formState.status === 'error' ? 'red' : 'green', + fontSize: '0.75rem', + fontWeight: 'normal', + }, + }, + formState.message, + ) + : null, + ), + ) +} diff --git a/exercises/05.actions/05.solution.history-revalidation/ui/error-boundary.js b/exercises/05.actions/05.solution.history-revalidation/ui/error-boundary.js new file mode 100644 index 0000000..f33036c --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/ui/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export * from 'react-error-boundary' diff --git a/exercises/05.actions/05.solution.history-revalidation/ui/img-utils.js b/exercises/05.actions/05.solution.history-revalidation/ui/img-utils.js new file mode 100644 index 0000000..ef8f4e9 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/ui/img-utils.js @@ -0,0 +1,5 @@ +export const shipFallbackSrc = '/img/fallback-ship.png' + +export function getImageUrlForShip(shipId, { size }) { + return `/img/ships/${shipId}.webp?size=${size}` +} diff --git a/exercises/05.actions/05.solution.history-revalidation/ui/img.js b/exercises/05.actions/05.solution.history-revalidation/ui/img.js new file mode 100644 index 0000000..84e6e40 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/ui/img.js @@ -0,0 +1,41 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' + +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 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/05.actions/05.solution.history-revalidation/ui/index.js b/exercises/05.actions/05.solution.history-revalidation/ui/index.js new file mode 100644 index 0000000..d92107f --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/ui/index.js @@ -0,0 +1,178 @@ +import { + Suspense, + createElement as h, + startTransition, + use, + useDeferredValue, + useEffect, + useRef, + useState, + useTransition, +} from 'react' +import { createRoot } from 'react-dom/client' +import * as RSC from 'react-server-dom-esm/client' +import { contentCache, useContentCache, generateKey } from './content-cache.js' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { RouterContext, getGlobalLocation, useLinkHandler } from './router.js' + +function fetchContent(location) { + return fetch(`/rsc${location}`) +} + +function updateContentKey() { + console.error('updateContentKey called before it was set!') +} + +function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { + moduleBaseURL: `${window.location.origin}/ui`, + callServer, + }) +} + +async function callServer(id, args) { + const fetchPromise = fetch(`/action${getGlobalLocation()}`, { + method: 'POST', + headers: { 'rsc-action': id }, + body: await RSC.encodeReply(args), + }) + const contentKey = window.history.state?.key ?? generateKey() + onStreamFinished(fetchPromise, () => { + updateContentKey(contentKey) + }) + const actionResponsePromise = createFromFetch(fetchPromise) + contentCache.set(contentKey, actionResponsePromise) + const { returnValue } = await actionResponsePromise + return returnValue +} + +const initialLocation = getGlobalLocation() +const initialContentPromise = createFromFetch(fetchContent(initialLocation)) + +let initialContentKey = window.history.state?.key +if (!initialContentKey) { + initialContentKey = generateKey() + window.history.replaceState({ key: initialContentKey }, '') +} +contentCache.set(initialContentKey, initialContentPromise) + +function onStreamFinished(fetchPromise, onFinished) { + // create a promise chain that resolves when the stream is completely consumed + return ( + fetchPromise + // clone the response so createFromFetch can use it (otherwise we lock the reader) + // and wait for the text to be consumed so we know the stream is finished + .then((response) => response.clone().text()) + .then(onFinished) + ) +} + +function Root() { + const latestNav = useRef(null) + const contentCache = useContentCache() + const [nextLocation, setNextLocation] = useState(getGlobalLocation) + const [contentKey, setContentKey] = useState(initialContentKey) + const [isPending, startTransition] = useTransition() + + // set the updateContentKey function in a useEffect to avoid issues with + // concurrent rendering (useDeferredValue will create throw-away renders). + useEffect(() => { + updateContentKey = (newContentKey) => { + startTransition(() => setContentKey(newContentKey)) + } + }, []) + + const location = useDeferredValue(nextLocation) + const contentPromise = contentCache.get(contentKey) + + useEffect(() => { + function handlePopState() { + const nextLocation = getGlobalLocation() + setNextLocation(nextLocation) + const historyKey = window.history.state?.key ?? generateKey() + + let nextContentPromise + const fetchPromise = fetchContent(nextLocation) + onStreamFinished(fetchPromise, () => { + startTransition(() => contentCache.set(historyKey, nextContentPromise)) + }) + nextContentPromise = createFromFetch(fetchPromise) + + if (!contentCache.has(historyKey)) { + // if we don't have this key in the cache already, set it now + contentCache.set(historyKey, nextContentPromise) + } + + updateContentKey(historyKey) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, [contentCache]) + + function navigate(nextLocation, { replace = false } = {}) { + setNextLocation(nextLocation) + const thisNav = Symbol(`Nav for ${nextLocation}`) + latestNav.current = thisNav + + const newContentKey = generateKey() + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then((response) => { + if (thisNav !== latestNav.current) return + if (replace) { + window.history.replaceState({ key: newContentKey }, '', nextLocation) + } else { + window.history.pushState({ key: newContentKey }, '', nextLocation) + } + return response + }), + ) + + contentCache.set(newContentKey, nextContentPromise) + updateContentKey(newContentKey) + } + + useLinkHandler(navigate) + + return h( + RouterContext, + { + value: { + navigate, + location, + nextLocation, + isPending, + }, + }, + use(contentPromise).root, + ) +} + +startTransition(() => { + createRoot(document.getElementById('root')).render( + h( + 'div', + { className: 'app-wrapper' }, + 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(Root), + ), + ), + ), + ) +}) diff --git a/exercises/05.actions/05.solution.history-revalidation/ui/router.js b/exercises/05.actions/05.solution.history-revalidation/ui/router.js new file mode 100644 index 0000000..e3e046c --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/ui/router.js @@ -0,0 +1,68 @@ +import { createContext, use, useEffect } 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 +} + +// Thanks Devon: https://twitter.com/devongovett/status/1672307153699471360 +export function useLinkHandler(navigate) { + useEffect(() => { + function onClick(event) { + const link = event.target.closest('a') + if ( + link && + link instanceof HTMLAnchorElement && + link.href && + (!link.target || link.target === '_self') && + link.origin === location.origin && + !link.hasAttribute('download') && + event.button === 0 && // left clicks only + !event.metaKey && // open in new tab (mac) + !event.ctrlKey && // open in new tab (windows) + !event.altKey && // download + !event.shiftKey && + !event.defaultPrevented + ) { + event.preventDefault() + navigate(link.pathname + link.search) + } + } + + document.addEventListener('click', onClick) + return () => { + document.removeEventListener('click', onClick) + } + }, [navigate]) +} + +export const getGlobalLocation = () => + window.location.pathname + window.location.search + +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/05.actions/05.solution.history-revalidation/ui/ship-details-pending.js b/exercises/05.actions/05.solution.history-revalidation/ui/ship-details-pending.js new file mode 100644 index 0000000..52bdc69 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/ui/ship-details-pending.js @@ -0,0 +1,20 @@ +'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 isShipDetailsPending = useSpinDelay( + parseLocationState(nextLocation).shipId !== + parseLocationState(location).shipId, + { delay: 300, minDuration: 350 }, + ) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/05.actions/05.solution.history-revalidation/ui/ship-details.js b/exercises/05.actions/05.solution.history-revalidation/ui/ship-details.js new file mode 100644 index 0000000..cd702fa --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/ui/ship-details.js @@ -0,0 +1,115 @@ +import { createElement as h } from 'react' +import { getShip } from '../db/ship-api.js' +import { shipDataStorage } from '../server/async-storage.js' +import { updateShipName } from './actions.js' +import { EditableText } from './edit-text.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, + h(EditableText, { + key: shipId, + shipId, + action: updateShipName, + initialValue: 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/05.actions/05.solution.history-revalidation/ui/ship-search-results.js b/exercises/05.actions/05.solution.history-revalidation/ui/ship-search-results.js new file mode 100644 index 0000000..b36ba30 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/ui/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 { SelectShipLink } 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( + SelectShipLink, + { 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/05.actions/05.solution.history-revalidation/ui/ship-search.js b/exercises/05.actions/05.solution.history-revalidation/ui/ship-search.js new file mode 100644 index 0000000..ce36358 --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/ui/ship-search.js @@ -0,0 +1,63 @@ +'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 isShipSearchPending = useSpinDelay( + parseLocationState(nextLocation).search !== + parseLocationState(location).search, + { 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: ShipResultsErrorFallback }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function SelectShipLink({ shipId, highlight, children }) { + const { location } = useRouter() + return h('a', { + children, + href: mergeLocationState(location, { shipId }), + style: { fontWeight: highlight ? 'bold' : 'normal' }, + }) +} + +export function ShipResultsErrorFallback() { + return h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ) +} diff --git a/exercises/05.actions/05.solution.history-revalidation/ui/spin-delay.js b/exercises/05.actions/05.solution.history-revalidation/ui/spin-delay.js new file mode 100644 index 0000000..5cfdb2b --- /dev/null +++ b/exercises/05.actions/05.solution.history-revalidation/ui/spin-delay.js @@ -0,0 +1,36 @@ +// copied from npm.im/spin-delay to make it easier to use in this workshop +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' +} diff --git a/exercises/05.actions/FINISHED.mdx b/exercises/05.actions/FINISHED.mdx new file mode 100644 index 0000000..2f5127b --- /dev/null +++ b/exercises/05.actions/FINISHED.mdx @@ -0,0 +1,5 @@ +# Server Actions + + + +๐Ÿ‘จโ€๐Ÿ’ผ Well done! You've now implemented server actions! diff --git a/exercises/05.actions/README.mdx b/exercises/05.actions/README.mdx new file mode 100644 index 0000000..3f483d3 --- /dev/null +++ b/exercises/05.actions/README.mdx @@ -0,0 +1,136 @@ +# Server Actions + + + +Most applications are better when you can interact with them and change the data +they display. This is called "mutation." + +On the web, mutations happen via the `
` element. When you submit a form, +the browser sends a request to the server, which processes the data and sends +back a response. + +The challenge with forms is there's a pretty significant amount of indirection. +The only thing connecting the form to the action it takes is the URL. If you're +a developer working on a web app, finding the code that handles a form +submission is often a challenge (and it's different in every app). + +In React's quest to "compose all the things," React has a solution to this +problem in the form of "server actions." + +Server actions are a subset of server functions. Server functions are similar to +client components in many ways. The RSC payload carries with it a reference to +the function you wish to perform and when the form is submitted, the server gets +that reference so it knows the right function to call. + +To expose these server functions to be called from the client, you use the +`'use server'` directive. Rather than rewriting the module to not contain the +function, the `'use server'` directive adds reference information so the RSC +payload generation can work. + +When that function is called, it can be wrapped inside a `startTransition` +which gives you a pending state for while that server function runs and that +transition is pending. + +To take it a step further, `useActionState` turns that server function into an +action which resembles the way native form actions work without the URL indirection. +This approach even supports progressive enhancement such that if the JavaScript +has not yet been loaded, the form will act like a normal form submission. + +In practice, what this all means is you get to pass a server-only function to a +client component and that component can call the function when it needs to. + +Here's an example of this: + +```jsx filename=ui/hug-koala.js +'use server' + +import { giveBigHug } from './utils.js' + +async function hugKoala(previousState, formData) { + const koalaName = formData.get('koalaName') + try { + await giveBigHug(koalaName) + return { status: 'success' } + } catch (error) { + return { status: 'error', error: error.message } + } +} +``` + +```jsx filename=ui/koala-form.js +'use client' + +import { useActionState } from 'react' +import { hugKoala } from './hug-koala.js' + +function KoalaForm() { + const [formState, formAction, isPending] = useActionState(hugKoala) + + return ( + + + + {formState.status === 'error' ?

{formState.error}

: null} + {formState.status === 'success' ?

Koala hugged!

: null} +
+ ) +} +``` + +That's all there is to it. For this to work, we need to make some client code +changes as well as server code changes. + +## Client Changes + +`react-server-dom-esm` is responsible for handling our server actions when +they're called. To do this, we configure a `callServer` function when we call +`createFromFetch`. This makes sense because the server is going to send the +actions references within the payload so its at that time that we need to +tell it what to do with those action references. + +The `callServer` function will receive an `id` and `args`. The `id` is the +server action reference and the `args` are the arguments that the server action +needs to run (the form data). + +```js +import * as RSC from 'react-server-dom-esm/client' + +function callServer(id, args) { + const fetchPromise = fetch(`/action${getGlobalLocation()}`, { + method: 'POST', + headers: { 'rsc-action': id }, + body: await RSC.encodeReply(args), + }) + // handle the fetchPromise similar to how we handle other content fetch + // promises +} +``` + +Things get a little tricky here because this needs to be defined outside our +component, but needs to update state within our component. That's an +implementation detail we'll explore within the exercise. + +## Server Changes + +The server needs to handle the action that the client is sending. This is done +by parsing the `rsc-action` header, parsing the request body and calling the +appropriate function with the given arguments. + +One part of this is the need to return the result of the action to the client +along with the updated RSC payload. So we change the payload from just the root +element to an object with `root` and `returnValue` properties which we can then +use on the client as needed. + +๐Ÿ“œ Relevant Docs: + +- [`use-server`](https://react.dev/reference/react/use-server) +- [Server Functions](https://19.react.dev/reference/rsc/server-functions) + + + Originally, there was a single concept called "server actions" but later this + was split into two concepts: "server actions" and "server functions". Server + actions are a subset of server functions, specifically for form submissions. + diff --git a/exercises/FINISHED.mdx b/exercises/FINISHED.mdx index 43336be..4431be3 100644 --- a/exercises/FINISHED.mdx +++ b/exercises/FINISHED.mdx @@ -1,3 +1,9 @@ # ๐Ÿคน React Server Components + + Hooray! You're all done! ๐Ÿ‘๐Ÿ‘ + +[Welcome to the ranks](https://twitter.com/kentcdodds/status/1773047467413495876) +of those of us who have implemented a React Server Components and Functions +framework! ๐ŸŽ‰ diff --git a/exercises/README.mdx b/exercises/README.mdx index f66cfa8..ad63e21 100644 --- a/exercises/README.mdx +++ b/exercises/README.mdx @@ -1 +1,150 @@ -# ๐Ÿคน React Server Components +# React Server Components ๐Ÿคน + + + +๐Ÿ‘จโ€๐Ÿ’ผ Hello, my name is Peter the Product Manager. I'm here to help you get +oriented and to give you your assignments for the workshop! + +Today we're going to implement a React framework based on React Server +Components and Functions! When we're finished with this, you'll have a deep +understanding of the primitives that power React Server Components and Functions +which is the future of React. + + + We're using unpublished versions of some React packages for this workshop. + We'll be dealing with implementation details that are not part of the public + React API and could change. But the concepts you're learning (which will be + our focus) will stick around forever. + + +We'll be building this framework without **any build tools**. This means no +TypeScript, no JSX, no Vite, nothing. We're going to be writing plain JavaScript +and using Node.js/browser native APIs to build our framework. + +While this isn't entirely ergonomic, it will help you separate what React is +offering us from the tools we use to build with React. + +The goal is for you to understand RSCs free of any abstractions so when you go +to a tool that supports RSCs, you'll have a deep understanding of what's going +on under the hood. + + + One of the challenges you're going to have to face is understanding the + difference between the requirements of React Server Components and Functions + vs our own choices in this specific implementation of RSCs. RSCs are a very + low-level primitive and there are lots of ways to accomplish the same thing. + Different implementations will have different trade-offs. Try to focus on the + overall concepts and not get too bogged down in the specifics of this + implementation. + + +Because we're working with such primitive APIs, this will be one of the more +challenging things you've done with React. Keep in mind that frameworks exist +that enable you to offer an awesome user experience with React. This workshop +is about understanding the primitives that power those frameworks now and in the +future. + +## What are React Server Components? + +Let's take a look at a flowchart showing a typical Single Page Application (SPA) +built with React: + +![A flowchart for a Typical SPA as described below](/images/spa.png) + +
+Here's a bullet-point text version of this flowchart: + +- User goes to site + + - Browser requests document + - Server responds with document + - Browser renders loading spinner + - Browser requests client code + - Server responds with client code + - Browser updates UI components + - Browser requests data + - Server generates data response + - Server sends JSON + - Browser updates UI with JSON data + +- User triggers state change (i.e., route change) + + - Browser renders pending UI + - Browser requests new data + - Server generates data response + - Server sends JSON + - Browser updates UI with JSON data + +- User triggers action (i.e., form submission) + - Browser renders pending UI + - Browser makes POST request + - Server performs action with POST body + - Server sends JSON + - Browser updates UI with JSON data + +
+ +React server components (RSCs) are a new feature in React that allows you to +stream React components from the server to the client. This means that instead +of requesting data from the server and then rendering it on the client, you can +make a request, have React generate the UI on the server, and send it back to +the client. + +![A flowchart for React Server Components and Functions as described below](/images/super-simple-rsc.png) + +
+Here's a bullet-point text version of the flowchart: + +- User goes to site + + - Browser requests document + - Server responds with document + - Browser renders Suspense fallback + - Browser requests JSX payload + - Server generates Serialized JSX with `react-server-dom-esm/server.renderToPipeableStream` + - Server streams Serialized JSX + - Browser renders streamed UI with `react-server-dom-esm/client.createFromFetch` + - Browser requests client component code + - Server responds with client component code + - Browser hydrates client components + +- User triggers state change (i.e., route change) NOTE: This only applies once + data starts streaming. Until then, state transitions act like a typical SPA. + + - Browser renders pending UI with `startTransition` + - Browser requests JSX payload + - Server generates Serialized JSX with `react-server-dom-esm/server.renderToPipeableStream` + - Server streams Serialized JSX + - Browser updates streamed UI with `react-server-dom-esm/client.createFromFetch` + +- User triggers action (i.e., form submission) + - Browser renders pending UI with `useActionState` + - Browser makes POST (via `callServer`) + - Server determines action and call with parsed request body + - Server streams Serialized JSX and action return value + - Browser updates streamed UI with `react-server-dom-esm/client.createFromFetch` + +
+ +There are very subtle differences in the flowchart above, but the impact of the +differences is profound. + +As a result of this architecture, no code for that UI needs to be sent to the +client and the data doesn't even need to be sent to the client either. This +solves some pretty significant performance and maintainability challenges with +SPAs or even server-rendered apps. + + + We'll be building this framework as a SPA that uses RSCs on the server. You + can absolutely enhance this further to support server rendering if you like, + but we're going to be skipping that optimization to keep things simple. + + +Let's go! + +๐ŸŽต Check out the workshop theme song! ๐ŸŽถ + + diff --git a/kcdshop/.diffignore b/kcdshop/.diffignore deleted file mode 100644 index 912d79f..0000000 --- a/kcdshop/.diffignore +++ /dev/null @@ -1 +0,0 @@ -*.webp diff --git a/kcdshop/deployed/Dockerfile b/kcdshop/deployed/Dockerfile deleted file mode 100644 index dc492b7..0000000 --- a/kcdshop/deployed/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM node:20-bookworm-slim as base - -RUN apt-get update && apt-get install -y git - -ENV KCDSHOP_CONTEXT_CWD="/myapp/workshop-content" -ENV KCDSHOP_DEPLOYED="true" -ENV KCDSHOP_DISABLE_WATCHER="true" -ENV FLY="true" -ENV PORT="8080" -ENV NODE_ENV="production" - -WORKDIR /myapp - -ADD . . - -RUN npm install --omit=dev - -CMD node ./setup-swap.js && \ - rm -rf ${KCDSHOP_CONTEXT_CWD} && \ - git clone https://github.com/epicweb-dev/react-server-components ${KCDSHOP_CONTEXT_CWD} && \ - cd ${KCDSHOP_CONTEXT_CWD} && \ - npx kcdshop start diff --git a/kcdshop/deployed/fly.toml b/kcdshop/deployed/fly.toml deleted file mode 100644 index e7418ae..0000000 --- a/kcdshop/deployed/fly.toml +++ /dev/null @@ -1,45 +0,0 @@ -app = "epicweb-dev-react-server-components" -primary_region = "sjc" -kill_signal = "SIGINT" -kill_timeout = 5 -processes = [ ] - -[experimental] -allowed_public_ports = [ ] -auto_rollback = true - -[[services]] -internal_port = 8080 -processes = [ "app" ] -protocol = "tcp" -script_checks = [ ] - - [services.concurrency] - hard_limit = 100 - soft_limit = 80 - type = "connections" - - [[services.ports]] - handlers = [ "http" ] - port = 80 - force_https = true - - [[services.ports]] - handlers = [ "tls", "http" ] - port = 443 - - [[services.tcp_checks]] - grace_period = "1s" - interval = "15s" - restart_limit = 0 - timeout = "2s" - - [[services.http_checks]] - interval = "10s" - grace_period = "5s" - method = "get" - path = "/" - protocol = "http" - timeout = "2s" - tls_skip_verify = false - headers = { } \ No newline at end of file diff --git a/kcdshop/deployed/package.json b/kcdshop/deployed/package.json deleted file mode 100644 index 5d7278e..0000000 --- a/kcdshop/deployed/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "deployed-react-server-components", - "private": true, - "type": "module", - "scripts": { - "start": "kcdshop start" - }, - "author": "Kent C. Dodds (https://kentcdodds.com/)", - "license": "GPL-3.0-only", - "dependencies": { - "@kentcdodds/workshop-app": "^3.13.0", - "execa": "^7.2.0" - }, - "engines": { - "node": ">=18", - "npm": ">=8.16.0" - } -} diff --git a/kcdshop/deployed/setup-swap.js b/kcdshop/deployed/setup-swap.js deleted file mode 100644 index 574aea1..0000000 --- a/kcdshop/deployed/setup-swap.js +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env node - -import { writeFile, stat } from 'node:fs/promises' -import { $ } from 'execa' - -const swapExists = await stat('/swapfile').catch(() => false) - -console.log('setting up swapfile...') - -if (swapExists) { - console.log('swapfile already exists') -} else { - await $`fallocate -l 128M /swapfile` - await $`chmod 0600 /swapfile` - await $`mkswap /swapfile` - await writeFile('/proc/sys/vm/swappiness', '10') - await $`swapon /swapfile` - await writeFile('/proc/sys/vm/overcommit_memory', '1') - console.log('swapfile setup complete') -} diff --git a/kcdshop/fix.js b/kcdshop/fix.js deleted file mode 100644 index 2241fd1..0000000 --- a/kcdshop/fix.js +++ /dev/null @@ -1,90 +0,0 @@ -// This should run by node without any dependencies -// because you may need to run it without deps. - -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const here = (...p) => path.join(__dirname, ...p) -// const VERBOSE = false -// const logVerbose = (...args) => (VERBOSE ? console.log(...args) : undefined) - -const workshopRoot = here('..') -const examples = (await readDir(here('../examples'))).map(dir => - here(`../examples/${dir}`), -) -const exercises = (await readDir(here('../exercises'))) - .map(name => here(`../exercises/${name}`)) - .filter(filepath => fs.statSync(filepath).isDirectory()) -const exerciseApps = ( - await Promise.all( - exercises.flatMap(async exercise => { - return (await readDir(exercise)) - .filter(dir => { - return /(problem|solution)/.test(dir) - }) - .map(dir => path.join(exercise, dir)) - }), - ) -).flat() -const exampleApps = (await readDir(here('../examples'))).map(dir => - here(`../examples/${dir}`), -) -const apps = [...exampleApps, ...exerciseApps] - -const appsWithPkgJson = [...examples, ...apps].filter(app => { - const pkgjsonPath = path.join(app, 'package.json') - return exists(pkgjsonPath) -}) - -// update the package.json file name property -// to match the parent directory name + directory name -// e.g. exercises/01-goo/problem.01-great -// name: "exercises__sep__01-goo.problem__sep__01-great" - -function relativeToWorkshopRoot(dir) { - return dir.replace(`${workshopRoot}${path.sep}`, '') -} - -await updatePkgNames() - -async function updatePkgNames() { - for (const file of appsWithPkgJson) { - const pkgjsonPath = path.join(file, 'package.json') - const pkg = JSON.parse(await fs.promises.readFile(pkgjsonPath, 'utf8')) - pkg.name = relativeToWorkshopRoot(file).replace(/\\|\//g, '__sep__') - const written = await writeIfNeeded( - pkgjsonPath, - `${JSON.stringify(pkg, null, 2)}\n`, - ) - if (written) { - console.log(`updated ${path.relative(process.cwd(), pkgjsonPath)}`) - } - } -} - -async function writeIfNeeded(filepath, content) { - const oldContent = await fs.promises.readFile(filepath, 'utf8') - if (oldContent !== content) { - await fs.promises.writeFile(filepath, content) - } - return oldContent !== content -} - -function exists(p) { - if (!p) return false - try { - fs.statSync(p) - return true - } catch (error) { - return false - } -} - -async function readDir(dir) { - if (exists(dir)) { - return fs.promises.readdir(dir) - } - return [] -} diff --git a/kcdshop/package-lock.json b/kcdshop/package-lock.json deleted file mode 100644 index b0ad41f..0000000 --- a/kcdshop/package-lock.json +++ /dev/null @@ -1,9490 +0,0 @@ -{ - "name": "kcdshop", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@kentcdodds/workshop-app": "^3.13.0", - "@kentcdodds/workshop-utils": "^3.13.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", - "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", - "dependencies": { - "@babel/highlight": "^7.24.2", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.2", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", - "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", - "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bundled-es-modules/cookie": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", - "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==", - "dependencies": { - "cookie": "^0.5.0" - } - }, - "node_modules/@bundled-es-modules/cookie/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@bundled-es-modules/statuses": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", - "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", - "dependencies": { - "statuses": "^2.0.1" - } - }, - "node_modules/@epic-web/cachified": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@epic-web/cachified/-/cachified-5.1.2.tgz", - "integrity": "sha512-8Q1J/jF0bOKUN+XPTSUo+z34WJHMBLkuDv+HkPPS9ufs+cHvInWrLOLi3qIljhe03Xq777pwEDBKnMafetnPhA==" - }, - "node_modules/@epic-web/invariant": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", - "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==" - }, - "node_modules/@epic-web/remember": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@epic-web/remember/-/remember-1.0.2.tgz", - "integrity": "sha512-K7DcGoRPqVkjVhPEMQzqw7W/c3hq/3LuiI74he6SkXwR6A49aUmXpxmdb6o+NldY4FFtG42U7nL8PrqNGRxXuQ==" - }, - "node_modules/@esbuild-plugins/node-resolve": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-resolve/-/node-resolve-0.2.2.tgz", - "integrity": "sha512-+t5FdX3ATQlb53UFDBRb4nqjYBz492bIrnVWvpQHpzZlu9BQL5HasMZhqc409ygUwOWCXZhrWr6NyZ6T6Y+cxw==", - "dependencies": { - "@types/resolve": "^1.17.1", - "debug": "^4.3.1", - "escape-string-regexp": "^4.0.0", - "resolve": "^1.19.0" - }, - "peerDependencies": { - "esbuild": "*" - } - }, - "node_modules/@esbuild-plugins/node-resolve/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@esbuild-plugins/node-resolve/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@esbuild-plugins/node-resolve/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", - "cpu": [ - "loong64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", - "cpu": [ - "mips64el" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@fal-works/esbuild-plugin-global-externals": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz", - "integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==" - }, - "node_modules/@floating-ui/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", - "integrity": "sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==", - "dependencies": { - "@floating-ui/utils": "^0.2.1" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.3.tgz", - "integrity": "sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==", - "dependencies": { - "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.8.tgz", - "integrity": "sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==", - "dependencies": { - "@floating-ui/dom": "^1.6.1" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", - "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" - }, - "node_modules/@inquirer/confirm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.1.0.tgz", - "integrity": "sha512-nH5mxoTEoqk6WpoBz80GMpDSm9jH5V9AF8n+JZAZfMzd9gHeEG9w1o3KawPRR72lfzpP+QxBHLkOKLEApwhDiQ==", - "dependencies": { - "@inquirer/core": "^7.1.0", - "@inquirer/type": "^1.2.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/core": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-7.1.0.tgz", - "integrity": "sha512-FRCiDiU54XHt5B/D8hX4twwZuzSP244ANHbu3R7CAsJfiv1dUOz24ePBgCZjygEjDUi6BWIJuk4eWLKJ7LATUw==", - "dependencies": { - "@inquirer/type": "^1.2.1", - "@types/mute-stream": "^0.0.4", - "@types/node": "^20.11.26", - "@types/wrap-ansi": "^3.0.0", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "cli-spinners": "^2.9.2", - "cli-width": "^4.1.0", - "figures": "^3.2.0", - "mute-stream": "^1.0.0", - "run-async": "^3.0.0", - "signal-exit": "^4.1.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/core/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/@inquirer/core/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/@inquirer/core/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/@inquirer/core/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/@inquirer/core/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/@inquirer/core/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/@inquirer/core/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/@inquirer/core/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/@inquirer/core/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/@inquirer/core/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@inquirer/type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.2.1.tgz", - "integrity": "sha512-xwMfkPAxeo8Ji/IxfUSqzRi0/+F2GIqJmpc5/thelgMGsjNZcjDDRBO9TLXT1s/hdx/mK5QbVIvgoLIFgXhTMQ==", - "engines": { - "node": ">=18" - } - }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@kentcdodds/md-temp": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@kentcdodds/md-temp/-/md-temp-8.0.1.tgz", - "integrity": "sha512-LT/kPFZf2IcfM9NqLHtKNRtrQa+G96xX67AOGTgd0tAk517dyHhQkymfNa0AM+ppKFRX2otKajgnr2gDhvyYNw==", - "dependencies": { - "escape-goat": "^4.0.0", - "parse-numeric-range": "^1.3.0", - "shiki": "^1.1.7", - "tinypool": "^0.8.2", - "unified": "^11.0.4", - "unist-util-visit": "^5.0.0" - } - }, - "node_modules/@kentcdodds/workshop-app": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@kentcdodds/workshop-app/-/workshop-app-3.13.0.tgz", - "integrity": "sha512-umKYDn6tjmOIi4xU4wUozW89uE8YlhpQqyfL7qe0knE4x4ojx4w6Qzp+pjJbRGsIeU3BKiefGHW+qNXyNeIKMA==", - "bundleDependencies": [ - "@conform-to/react", - "@conform-to/zod", - "@epic-web/client-hints", - "@epic-web/restore-scroll", - "@radix-ui/react-accordion", - "@radix-ui/react-popover", - "@radix-ui/react-select", - "@radix-ui/react-tabs", - "@radix-ui/react-toast", - "@remix-run/react", - "@remix-run/router", - "react-router", - "react-router-dom", - "clsx", - "chai", - "chai-dom", - "framer-motion", - "remix-utils", - "tailwind-merge", - "zod" - ], - "dependencies": { - "@conform-to/react": "^1.0.3", - "@conform-to/zod": "^1.0.3", - "@epic-web/cachified": "^5.1.2", - "@epic-web/client-hints": "^1.3.0", - "@epic-web/invariant": "^1.0.0", - "@epic-web/remember": "^1.0.2", - "@epic-web/restore-scroll": "^1.0.1", - "@kentcdodds/workshop-presence": "3.13.0", - "@kentcdodds/workshop-utils": "3.13.0", - "@mdx-js/mdx": "^3.0.1", - "@mux/mux-player-react": "^2.3.3", - "@paralleldrive/cuid2": "^2.2.2", - "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-popover": "^1.0.7", - "@radix-ui/react-select": "^2.0.0", - "@radix-ui/react-tabs": "^1.0.4", - "@radix-ui/react-toast": "^1.1.5", - "@radix-ui/react-tooltip": "^1.0.7", - "@remix-run/css-bundle": "^2.8.1", - "@remix-run/express": "^2.8.1", - "@remix-run/node": "^2.8.1", - "@remix-run/react": "^2.8.1", - "@remix-run/router": "*", - "@sindresorhus/slugify": "^2.2.1", - "@types/chai": "^4.3.12", - "@types/chai-dom": "^1.11.3", - "address": "^2.0.2", - "ansi-to-html": "^0.7.2", - "chai": "^5.1.0", - "chai-dom": "^1.12.0", - "chalk": "^5.3.0", - "chokidar": "^3.6.0", - "close-with-grace": "^1.3.0", - "clsx": "^2.1.0", - "compression": "^1.7.4", - "confetti-react": "^2.5.0", - "cookie": "^0.6.0", - "cross-env": "^7.0.3", - "cross-spawn": "^7.0.3", - "dayjs": "^1.11.10", - "dotenv": "^16.4.5", - "esbuild": "^0.20.1", - "etag": "^1.8.1", - "execa": "^8.0.1", - "express": "^4.18.3", - "fkill": "^9.0.0", - "framer-motion": "^11.0.8", - "fs-extra": "^11.2.0", - "get-port": "^7.0.0", - "glob": "^10.3.10", - "globby": "^14.0.1", - "ignore": "^5.3.1", - "isbot": "^5.1.1", - "lodash.escape": "^4.0.1", - "lru-cache": "^10.2.0", - "md5-hex": "^5.0.0", - "mdx-bundler": "^10.0.1", - "mime-types": "^2.1.35", - "morgan": "^1.10.0", - "msw": "^2.2.3", - "openid-client": "^5.6.5", - "p-queue": "^8.0.1", - "parse-git-diff": "^0.0.15", - "partysocket": "^1.0.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router": "*", - "react-router-dom": "*", - "remark-autolink-headings": "^7.0.1", - "remark-emoji": "^4.0.1", - "remark-gfm": "^4.0.0", - "remix-flat-routes": "^0.6.4", - "remix-utils": "7.5.0", - "shell-quote": "^1.8.1", - "shiki": "^1.1.7", - "sonner": "^1.4.3", - "source-map-support": "^0.5.21", - "spin-delay": "^1.2.0", - "tailwind-merge": "^2.2.1", - "tailwindcss-radix": "^2.8.0", - "unified": "^11.0.4", - "unist-util-remove-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "ws": "^8.16.0", - "zod": "^3.22.4" - }, - "bin": { - "kcdshop": "bin/kcdshop.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@babel/runtime": { - "version": "7.24.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@conform-to/dom": { - "version": "1.0.4", - "inBundle": true, - "license": "MIT" - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@conform-to/react": { - "version": "1.0.4", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@conform-to/dom": "1.0.4" - }, - "peerDependencies": { - "react": ">=18" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@conform-to/zod": { - "version": "1.0.4", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@conform-to/dom": "1.0.4" - }, - "peerDependencies": { - "zod": "^3.21.0" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@epic-web/client-hints": { - "version": "1.3.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@epic-web/restore-scroll": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "peerDependencies": { - "react": ">=18.0.0", - "react-router-dom": ">=6.4.0" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@floating-ui/core": { - "version": "1.6.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.1" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@floating-ui/dom": { - "version": "1.6.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.0.0", - "@floating-ui/utils": "^0.2.0" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@floating-ui/react-dom": { - "version": "2.0.8", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.6.1" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@floating-ui/utils": { - "version": "0.2.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/number": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/primitive": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-accordion": { - "version": "1.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collapsible": "1.0.3", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-arrow": { - "version": "1.0.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-collapsible": { - "version": "1.0.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-collection": { - "version": "1.0.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-direction": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.4", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-popover": { - "version": "1.0.7", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-popper": { - "version": "1.1.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-portal": { - "version": "1.0.4", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-presence": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-roving-focus": { - "version": "1.0.4", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-select": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/number": "1.0.1", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-previous": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-tabs": { - "version": "1.0.4", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-use-controllable-state": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-toast": { - "version": "1.1.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-use-previous": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-use-rect": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-use-size": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/react-visually-hidden": { - "version": "1.0.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@radix-ui/rect": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@remix-run/react": { - "version": "2.8.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.15.3", - "@remix-run/server-runtime": "2.8.1", - "react-router": "6.22.3", - "react-router-dom": "6.22.3" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0", - "typescript": "^5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@remix-run/react/node_modules/@remix-run/router": { - "version": "1.15.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@remix-run/server-runtime": { - "version": "2.8.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.15.3", - "@types/cookie": "^0.6.0", - "@web3-storage/multipart-parser": "^1.0.0", - "cookie": "^0.6.0", - "set-cookie-parser": "^2.4.8", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "typescript": "^5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@remix-run/server-runtime/node_modules/@remix-run/router": { - "version": "1.15.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@types/cookie": { - "version": "0.6.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/@kentcdodds/workshop-app/node_modules/@web3-storage/multipart-parser": { - "version": "1.0.0", - "inBundle": true, - "license": "(Apache-2.0 AND MIT)" - }, - "node_modules/@kentcdodds/workshop-app/node_modules/aria-hidden": { - "version": "1.2.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/assertion-error": { - "version": "2.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/chai": { - "version": "5.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.0.0", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/chai-dom": { - "version": "1.12.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.12.0" - }, - "peerDependencies": { - "chai": ">= 3" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/check-error": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/clsx": { - "version": "2.1.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/cookie": { - "version": "0.6.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/deep-eql": { - "version": "5.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/detect-node-es": { - "version": "1.1.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/@kentcdodds/workshop-app/node_modules/framer-motion": { - "version": "11.0.12", - "inBundle": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0", - "react-dom": "^18.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/get-func-name": { - "version": "2.0.2", - "inBundle": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/get-nonce": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/invariant": { - "version": "2.2.4", - "inBundle": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/js-tokens": { - "version": "4.0.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/@kentcdodds/workshop-app/node_modules/loose-envify": { - "version": "1.4.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/loupe": { - "version": "3.1.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/pathval": { - "version": "2.0.0", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/react-remove-scroll": { - "version": "2.5.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/react-remove-scroll-bar": { - "version": "2.3.5", - "inBundle": true, - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.1", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/react-router": { - "version": "6.22.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.15.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/react-router-dom": { - "version": "6.22.3", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@remix-run/router": "1.15.3", - "react-router": "6.22.3" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/react-router-dom/node_modules/@remix-run/router": { - "version": "1.15.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/react-router/node_modules/@remix-run/router": { - "version": "1.15.3", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/react-style-singleton": { - "version": "2.2.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "invariant": "^2.2.4", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/regenerator-runtime": { - "version": "0.14.1", - "inBundle": true, - "license": "MIT" - }, - "node_modules/@kentcdodds/workshop-app/node_modules/remix-utils": { - "version": "7.5.0", - "inBundle": true, - "license": "MIT", - "dependencies": { - "type-fest": "^4.3.3" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@remix-run/cloudflare": "^2.0.0", - "@remix-run/deno": "^2.0.0", - "@remix-run/node": "^2.0.0", - "@remix-run/react": "^2.0.0", - "@remix-run/router": "^1.7.2", - "crypto-js": "^4.1.1", - "intl-parse-accept-language": "^1.0.0", - "is-ip": "^5.0.1", - "react": "^18.0.0", - "zod": "^3.22.4" - }, - "peerDependenciesMeta": { - "@remix-run/cloudflare": { - "optional": true - }, - "@remix-run/deno": { - "optional": true - }, - "@remix-run/node": { - "optional": true - }, - "@remix-run/react": { - "optional": true - }, - "@remix-run/router": { - "optional": true - }, - "crypto-js": { - "optional": true - }, - "intl-parse-accept-language": { - "optional": true - }, - "is-ip": { - "optional": true - }, - "react": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/remix-utils/node_modules/type-fest": { - "version": "4.12.0", - "inBundle": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/set-cookie-parser": { - "version": "2.6.0", - "inBundle": true, - "license": "MIT" - }, - "node_modules/@kentcdodds/workshop-app/node_modules/source-map": { - "version": "0.6.1", - "inBundle": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/tailwind-merge": { - "version": "2.2.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.7" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/tslib": { - "version": "2.6.2", - "inBundle": true, - "license": "0BSD" - }, - "node_modules/@kentcdodds/workshop-app/node_modules/use-callback-ref": { - "version": "1.3.1", - "inBundle": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/use-sidecar": { - "version": "1.1.2", - "inBundle": true, - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@kentcdodds/workshop-app/node_modules/zod": { - "version": "3.22.4", - "inBundle": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/@kentcdodds/workshop-presence": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@kentcdodds/workshop-presence/-/workshop-presence-3.13.0.tgz", - "integrity": "sha512-B5Ki5BFvSp4tv0tBkVbKwhAuZSszIQpEgUo0ywYzM1UOlJdaeQL3rjFl+w1c7afqfLWwpGU92y69WJBBRlBMsA==", - "dependencies": { - "@kentcdodds/workshop-utils": "3.13.0", - "zod": "^3.22.4" - } - }, - "node_modules/@kentcdodds/workshop-utils": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@kentcdodds/workshop-utils/-/workshop-utils-3.13.0.tgz", - "integrity": "sha512-9i6FrwnHMlgFvHLo5iKn1a/x0Y0XFom8zsqpH1myrjhsZ/Ynnrr0yTZkFZ7BgDPpgDGWez0dSikT6p3nDHEBnQ==", - "dependencies": { - "@epic-web/cachified": "^5.1.2", - "@epic-web/remember": "^1.0.2", - "@kentcdodds/md-temp": "^8.0.1", - "@mdx-js/mdx": "^3.0.1", - "@playwright/test": "^1.42.1", - "@remix-run/node": "^2.8.1", - "@testing-library/dom": "^9.3.4", - "@total-typescript/ts-reset": "^0.5.1", - "chai": "^5.1.0", - "chai-dom": "^1.12.0", - "chalk": "^5.3.0", - "chokidar": "^3.6.0", - "close-with-grace": "^1.3.0", - "cross-spawn": "^7.0.3", - "execa": "^8.0.1", - "fkill": "^9.0.0", - "fs-extra": "^11.2.0", - "glob": "^10.3.10", - "globby": "^14.0.1", - "lru-cache": "^10.2.0", - "md5-hex": "^5.0.0", - "mdast-util-mdx-jsx": "^3.1.1", - "mdx-bundler": "^10.0.1", - "p-queue": "^8.0.1", - "rehype": "^13.0.1", - "remark": "^15.0.1", - "remark-autolink-headings": "^7.0.1", - "remark-emoji": "^4.0.1", - "remark-gfm": "^4.0.0", - "unified": "^11.0.4", - "unist-util-remove-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "zod": "^3.22.4" - } - }, - "node_modules/@mdx-js/esbuild": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@mdx-js/esbuild/-/esbuild-3.0.1.tgz", - "integrity": "sha512-+KZbCKcRjFtRD6qzD+c70Vq/VPVt5LHFsOshNcsdcONkaLTCSjmM7/uj71i3BcP+170f+P4DwVEMtqR/k0t5aw==", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/unist": "^3.0.0", - "vfile": "^6.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "esbuild": ">=0.14.0" - } - }, - "node_modules/@mdx-js/mdx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.0.1.tgz", - "integrity": "sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA==", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdx": "^2.0.0", - "collapse-white-space": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-build-jsx": "^3.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-util-to-js": "^2.0.0", - "estree-walker": "^3.0.0", - "hast-util-to-estree": "^3.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "markdown-extensions": "^2.0.0", - "periscopic": "^3.0.0", - "remark-mdx": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "source-map": "^0.7.0", - "unified": "^11.0.0", - "unist-util-position-from-estree": "^2.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@mswjs/cookies": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@mswjs/cookies/-/cookies-1.1.0.tgz", - "integrity": "sha512-0ZcCVQxifZmhwNBoQIrystCb+2sWBY2Zw8lpfJBPCHGCA/HWqehITeCRVIv4VMy8MPlaHo2w2pTHFV2pFfqKPw==", - "engines": { - "node": ">=18" - } - }, - "node_modules/@mswjs/interceptors": { - "version": "0.25.16", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.25.16.tgz", - "integrity": "sha512-8QC8JyKztvoGAdPgyZy49c9vSHHAZjHagwl4RY9E8carULk8ym3iTaiawrT1YoLF/qb449h48f71XDPgkUSOUg==", - "dependencies": { - "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/logger": "^0.3.0", - "@open-draft/until": "^2.0.0", - "is-node-process": "^1.2.0", - "outvariant": "^1.2.1", - "strict-event-emitter": "^0.5.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@mux/mux-player": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@mux/mux-player/-/mux-player-2.4.0.tgz", - "integrity": "sha512-6F6RvXyIWwYtwh29+bmoqa5Nd1z31wGafeKtIds0BYP+PdVYRSRhQ16B8tajUb0+yeCVbqyvBnITEUoAZ0zuIQ==", - "dependencies": { - "@mux/mux-video": "0.17.4", - "@mux/playback-core": "0.22.3", - "media-chrome": "~3.1.1" - } - }, - "node_modules/@mux/mux-player-react": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@mux/mux-player-react/-/mux-player-react-2.4.0.tgz", - "integrity": "sha512-N0zHfNYXo9eNHLlYcHydLDFHk8TQpt4Psi2MyjIn7o8IR2dDsTHNxF5nxVTAMtCx7owohePgXa8g5nvXMixORQ==", - "dependencies": { - "@mux/mux-player": "2.4.0", - "@mux/playback-core": "0.22.3", - "prop-types": "^15.7.2" - }, - "peerDependencies": { - "@types/react": "^17.0.0 || ^18", - "react": "^17.0.2 || ^18", - "react-dom": "^17.0.2 || ^18" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@mux/mux-video": { - "version": "0.17.4", - "resolved": "https://registry.npmjs.org/@mux/mux-video/-/mux-video-0.17.4.tgz", - "integrity": "sha512-ePTL+Kxtw03OmCQ6y34A8tTdK4ctsHPzuOY7etCFldhdAMTomwxSacdNwa8BpZ23umkXHp+FQXPyJQIfv4G++Q==", - "dependencies": { - "@mux/playback-core": "0.22.3", - "castable-video": "~1.0.6", - "custom-media-element": "~1.2.3", - "media-tracks": "~0.3.0" - } - }, - "node_modules/@mux/playback-core": { - "version": "0.22.3", - "resolved": "https://registry.npmjs.org/@mux/playback-core/-/playback-core-0.22.3.tgz", - "integrity": "sha512-IZBMMhUKsEtSrxb8XyhfJVzI9g6Ca0poCDrJQy9hCV3+DmgfFPDLSxZZvtSglH5CsmFPHL+hOFD/qn9ewCmAOA==", - "dependencies": { - "hls.js": "~1.4.13", - "mux-embed": "~4.30.0" - } - }, - "node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@open-draft/deferred-promise": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", - "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==" - }, - "node_modules/@open-draft/logger": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", - "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", - "dependencies": { - "is-node-process": "^1.2.0", - "outvariant": "^1.4.0" - } - }, - "node_modules/@open-draft/until": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", - "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==" - }, - "node_modules/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", - "dependencies": { - "@noble/hashes": "^1.1.5" - } - }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@playwright/test": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", - "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", - "dependencies": { - "playwright": "1.42.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@radix-ui/primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", - "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", - "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz", - "integrity": "sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz", - "integrity": "sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", - "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz", - "integrity": "sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", - "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.3.tgz", - "integrity": "sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.4.tgz", - "integrity": "sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", - "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz", - "integrity": "sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.5", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.3", - "@radix-ui/react-portal": "1.0.4", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", - "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", - "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", - "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/rect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", - "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", - "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", - "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", - "dependencies": { - "@babel/runtime": "^7.13.10" - } - }, - "node_modules/@remix-run/css-bundle": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/@remix-run/css-bundle/-/css-bundle-2.8.1.tgz", - "integrity": "sha512-rn72xyUJ+rR5I0IxjlDWPPBddV1kpLvOT2FY9SMIUTFqyxq3ZK2W7FCtFzBt2JZQGZ9oDYidXYMWkW6tXY//rg==", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@remix-run/express": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/@remix-run/express/-/express-2.8.1.tgz", - "integrity": "sha512-p1eo8uwZk8uLihSDpUnPOPsTDfghWikVPQfa+e0ZMk6tnJCjcpHAyENKDFtn9vDh9h7YNUg6A7+19CStHgxd7Q==", - "dependencies": { - "@remix-run/node": "2.8.1" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "express": "^4.17.1", - "typescript": "^5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@remix-run/node": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-2.8.1.tgz", - "integrity": "sha512-ddCwBVlfLvRxTQJHPcaM1lhfMjsFYG3EGmYpWJIWnnzDX5EbX9pUNHBWisMuH1eA0c7pbw0PbW0UtCttKYx2qg==", - "dependencies": { - "@remix-run/server-runtime": "2.8.1", - "@remix-run/web-fetch": "^4.4.2", - "@remix-run/web-file": "^3.1.0", - "@remix-run/web-stream": "^1.1.0", - "@web3-storage/multipart-parser": "^1.0.0", - "cookie-signature": "^1.1.0", - "source-map-support": "^0.5.21", - "stream-slice": "^0.1.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "typescript": "^5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@remix-run/router": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz", - "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@remix-run/server-runtime": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/@remix-run/server-runtime/-/server-runtime-2.8.1.tgz", - "integrity": "sha512-fh4SOEoONrN73Kvzc0gMDCmYpVRVbvoj9j3BUXHAcn0An8iX+HD/22gU7nTkIBzExM/F9xgEcwTewOnWqLw0Bg==", - "dependencies": { - "@remix-run/router": "1.15.3", - "@types/cookie": "^0.6.0", - "@web3-storage/multipart-parser": "^1.0.0", - "cookie": "^0.6.0", - "set-cookie-parser": "^2.4.8", - "source-map": "^0.7.3" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "typescript": "^5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@remix-run/v1-route-convention": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@remix-run/v1-route-convention/-/v1-route-convention-0.1.4.tgz", - "integrity": "sha512-fVTr9YlNLWfaiM/6Y56sOtcY8x1bBJQHY0sDWO5+Z/vjJ2Ni7fe2fwrzs1jUFciMPXqBQdFGePnkuiYLz3cuUA==", - "dependencies": { - "minimatch": "^7.4.3" - }, - "peerDependencies": { - "@remix-run/dev": "^1.15.0 || ^2.0.0" - } - }, - "node_modules/@remix-run/v1-route-convention/node_modules/minimatch": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", - "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@remix-run/web-blob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@remix-run/web-blob/-/web-blob-3.1.0.tgz", - "integrity": "sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g==", - "dependencies": { - "@remix-run/web-stream": "^1.1.0", - "web-encoding": "1.1.5" - } - }, - "node_modules/@remix-run/web-fetch": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@remix-run/web-fetch/-/web-fetch-4.4.2.tgz", - "integrity": "sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA==", - "dependencies": { - "@remix-run/web-blob": "^3.1.0", - "@remix-run/web-file": "^3.1.0", - "@remix-run/web-form-data": "^3.1.0", - "@remix-run/web-stream": "^1.1.0", - "@web3-storage/multipart-parser": "^1.0.0", - "abort-controller": "^3.0.0", - "data-uri-to-buffer": "^3.0.1", - "mrmime": "^1.0.0" - }, - "engines": { - "node": "^10.17 || >=12.3" - } - }, - "node_modules/@remix-run/web-file": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@remix-run/web-file/-/web-file-3.1.0.tgz", - "integrity": "sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ==", - "dependencies": { - "@remix-run/web-blob": "^3.1.0" - } - }, - "node_modules/@remix-run/web-form-data": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@remix-run/web-form-data/-/web-form-data-3.1.0.tgz", - "integrity": "sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A==", - "dependencies": { - "web-encoding": "1.1.5" - } - }, - "node_modules/@remix-run/web-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@remix-run/web-stream/-/web-stream-1.1.0.tgz", - "integrity": "sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA==", - "dependencies": { - "web-streams-polyfill": "^3.1.1" - } - }, - "node_modules/@shikijs/core": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.2.0.tgz", - "integrity": "sha512-OlFvx+nyr5C8zpcMBnSGir0YPD6K11uYhouqhNmm1qLiis4GA7SsGtu07r9gKS9omks8RtQqHrJL4S+lqWK01A==" - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sindresorhus/slugify": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", - "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", - "dependencies": { - "@sindresorhus/transliterate": "^1.0.0", - "escape-string-regexp": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@sindresorhus/transliterate": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", - "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", - "dependencies": { - "escape-string-regexp": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@testing-library/dom": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", - "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@testing-library/dom/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/@testing-library/dom/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/@testing-library/dom/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/@testing-library/dom/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/@testing-library/dom/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/@testing-library/dom/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/@total-typescript/ts-reset": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@total-typescript/ts-reset/-/ts-reset-0.5.1.tgz", - "integrity": "sha512-AqlrT8YA1o7Ff5wPfMOL0pvL+1X+sw60NN6CcOCqs658emD6RfiXhF7Gu9QcfKBH7ELY2nInLhKSCWVoNL70MQ==" - }, - "node_modules/@types/acorn": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", - "integrity": "sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==" - }, - "node_modules/@types/chai": { - "version": "4.3.14", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.14.tgz", - "integrity": "sha512-Wj71sXE4Q4AkGdG9Tvq1u/fquNz9EdG4LIJMwVVII7ashjD/8cf8fyIfJAjRr6YcsXnSE8cOGQPq1gqeR8z+3w==" - }, - "node_modules/@types/chai-dom": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@types/chai-dom/-/chai-dom-1.11.3.tgz", - "integrity": "sha512-EUEZI7uID4ewzxnU7DJXtyvykhQuwe+etJ1wwOiJyQRTH/ifMWKX+ghiXkxCUvNJ6IQDodf0JXhuP6zZcy2qXQ==", - "dependencies": { - "@types/chai": "*" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mdast": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz", - "integrity": "sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg==", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mdx": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.12.tgz", - "integrity": "sha512-H9VZ9YqE+H28FQVchC83RCs5xQ2J7mAAv6qdDEaWmXEVl3OpdH+xfrSUzQ1lp7U7oSTRZ0RvW08ASPJsYBi7Cw==" - }, - "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" - }, - "node_modules/@types/mute-stream": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", - "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "20.11.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", - "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/@types/resolve": { - "version": "1.20.6", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", - "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==" - }, - "node_modules/@types/statuses": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", - "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==" - }, - "node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==" - }, - "node_modules/@types/wrap-ansi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", - "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" - }, - "node_modules/@web3-storage/multipart-parser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz", - "integrity": "sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw==" - }, - "node_modules/@zxing/text-encoding": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", - "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", - "optional": true - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "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-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/address": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/address/-/address-2.0.2.tgz", - "integrity": "sha512-u6nFssvaX9RHQmjMSqgT7b7QJbf/5/U8+ntbTL8vgABfIiEmm02ZSM5MwljKjCrIrm7iIbgYEya2YW6AaRccVA==", - "engines": { - "node": ">= 16.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", - "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", - "dependencies": { - "clean-stack": "^5.2.0", - "indent-string": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ansi-to-html": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", - "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", - "dependencies": { - "entities": "^2.2.0" - }, - "bin": { - "ansi-to-html": "bin/ansi-to-html" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/aria-hidden": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", - "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "engines": { - "node": ">=12" - } - }, - "node_modules/astring": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz", - "integrity": "sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==", - "bin": { - "astring": "bin/astring" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "node_modules/basic-auth": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", - "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", - "dependencies": { - "safe-buffer": "5.1.2" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/blueimp-md5": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", - "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==" - }, - "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/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "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/castable-video": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/castable-video/-/castable-video-1.0.6.tgz", - "integrity": "sha512-Ykw2uL4ZQnqX0j9KF9ksbDpyc8I53mFMswCKW9yV5TrwpWkdNqRHLlcU85W30BIw61fgDjgm0Xh5G0rbcmv23g==", - "dependencies": { - "custom-media-element": "~1.2.2" - } - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chai": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.0.tgz", - "integrity": "sha512-kDZ7MZyM6Q1DhR9jy7dalKohXQ2yrlXkk59CR52aRKxJrobmlBNqnFQxX9xOX8w+4mz8SYlKJa/7D7ddltFXCw==", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.0.0", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/chai-dom": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/chai-dom/-/chai-dom-1.12.0.tgz", - "integrity": "sha512-pLP8h6IBR8z1AdeQ+EMcJ7dXPdsax/1Q7gdGZjsnAmSBl3/gItQUYSCo32br1qOy4SlcBjvqId7ilAf3uJ2K1w==", - "engines": { - "node": ">= 0.12.0" - }, - "peerDependencies": { - "chai": ">= 3" - } - }, - "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/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/check-error": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.0.0.tgz", - "integrity": "sha512-tjLAOBHKVxtPoHe/SA7kNOMvhCRdCJ3vETdeY0RuAc9popf+hyaSV6ZEg9hr4cpWF7jmo/JSWEnLDrnijS9Tog==", - "engines": { - "node": ">= 16" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/clean-stack": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", - "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", - "dependencies": { - "escape-string-regexp": "5.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "engines": { - "node": ">= 12" - } - }, - "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/cliui/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/cliui/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/cliui/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/cliui/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/cliui/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/cliui/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/cliui/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/close-with-grace": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/close-with-grace/-/close-with-grace-1.3.0.tgz", - "integrity": "sha512-lvm0rmLIR5bNz4CRKW6YvCfn9Wg5Wb9A8PJ3Bb+hjyikgC1RO1W3J4z9rBXQYw97mAte7dNSQI8BmUsxdlXQyw==" - }, - "node_modules/collapse-white-space": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", - "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "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/confetti-react": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/confetti-react/-/confetti-react-2.5.0.tgz", - "integrity": "sha512-MqzdSqiksBFFVxFaueC6PIbhGw9vU+FgXvSfOYXxXVSZnmkEzX+MTbAovcc+AUu0cMjpxAYPO5eBR5xIfKTQnA==", - "dependencies": { - "tween-functions": "^1.2.0" - }, - "peerDependencies": { - "react": "^16.3.0 || ^17.0.0" - } - }, - "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.2.1", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.1.tgz", - "integrity": "sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==", - "engines": { - "node": ">=6.6.0" - } - }, - "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/custom-media-element": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/custom-media-element/-/custom-media-element-1.2.3.tgz", - "integrity": "sha512-xr9Hbrslkjm1fapJP5hL98pySeZmNepBSefQS/XTxynamqPTfRBK5MnhReMOiAj8xvJApVPrVnlYxIrknay8jg==" - }, - "node_modules/data-uri-to-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", - "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", - "engines": { - "node": ">= 6" - } - }, - "node_modules/dayjs": { - "version": "1.11.10", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" - }, - "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/decode-named-character-reference": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", - "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/deep-eql": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz", - "integrity": "sha512-nwQCf6ne2gez3o1MxWifqkciwt0zhl0LO1/UwVu4uMBuPmflWM4oQ70XMqHqnBJA+nhzncaqL9HVL6KkHJ28lw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.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/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "engines": { - "node": ">=6" - } - }, - "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/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" - }, - "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" - }, - "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": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" - }, - "node_modules/emojilib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", - "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==" - }, - "node_modules/emoticon": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.0.1.tgz", - "integrity": "sha512-dqx7eA9YaqyvYtUhJwT4rC1HIp82j5ybS1/vQ42ur+jBe17dJMwZE4+gvL1XadSFfxaPFFGt3Xsw+Y8akThDlw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "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/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "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/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" - } - }, - "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-goat": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estree-util-attach-comments": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", - "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", - "dependencies": { - "@types/estree": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-build-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", - "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-walker": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-to-js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", - "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "astring": "^1.8.0", - "source-map": "^0.7.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-value-to-estree": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.0.1.tgz", - "integrity": "sha512-b2tdzTurEIbwRh+mKrEcaWfu1wgb8J1hVsgREg7FFiecWwK/PhO8X0kyc+0bIcKNtD4sqxIdNoRy6/p/TvECEA==", - "dependencies": { - "@types/estree": "^1.0.0", - "is-plain-obj": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/remcohaszing" - } - }, - "node_modules/estree-util-visit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", - "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "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/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" - }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "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/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" - }, - "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/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fault": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", - "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", - "dependencies": { - "format": "^0.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "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/fkill": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/fkill/-/fkill-9.0.0.tgz", - "integrity": "sha512-MdYSsbdCaIRjzo5edthZtWmEZVMfr1qrtYZUHIdO3swCE+CoZA8S5l0s4jDsYlTa9ZiXv0pTgpzE7s4N8NeUOA==", - "dependencies": { - "aggregate-error": "^5.0.0", - "execa": "^8.0.1", - "pid-port": "^1.0.0", - "process-exists": "^5.0.0", - "ps-list": "^8.1.1", - "taskkill": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "engines": { - "node": ">=0.4.x" - } - }, - "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/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "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/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "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-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "engines": { - "node": "*" - } - }, - "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-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "engines": { - "node": ">=6" - } - }, - "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/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globby": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", - "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", - "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.2", - "ignore": "^5.2.4", - "path-type": "^5.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "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/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/graphql": { - "version": "16.8.1", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.8.1.tgz", - "integrity": "sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==", - "engines": { - "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" - } - }, - "node_modules/gray-matter": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", - "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", - "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "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.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "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/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-util-from-html": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.1.tgz", - "integrity": "sha512-RXQBLMl9kjKVNkJTIO6bZyb2n+cUH8LFaSSzo82jiLT6Tfc+Pt7VQCS+/h3YwG4jaNE2TA2sdJisGWR+aJrp0g==", - "dependencies": { - "@types/hast": "^3.0.0", - "devlop": "^1.1.0", - "hast-util-from-parse5": "^8.0.0", - "parse5": "^7.0.0", - "vfile": "^6.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-from-parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz", - "integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "hastscript": "^8.0.0", - "property-information": "^6.0.0", - "vfile": "^6.0.0", - "vfile-location": "^5.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-raw": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.2.tgz", - "integrity": "sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-from-parse5": "^8.0.0", - "hast-util-to-parse5": "^8.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "parse5": "^7.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-estree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.0.tgz", - "integrity": "sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-attach-comments": "^3.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^0.4.0", - "unist-util-position": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-html": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.0.tgz", - "integrity": "sha512-IVGhNgg7vANuUA2XKrT6sOIIPgaYZnmLx3l/CCOAK0PtgfoHrZwX7jCSYyFxHTrGmC6S9q8aQQekjp4JPZF+cw==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-raw": "^9.0.0", - "hast-util-whitespace": "^3.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "stringify-entities": "^4.0.0", - "zwitch": "^2.0.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz", - "integrity": "sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-object": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime/node_modules/inline-style-parser": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.2.tgz", - "integrity": "sha512-EcKzdTHVe8wFVOGEYXiW9WmJXPjqi1T+234YpJr98RiFYKHV3cdy1+3mkTE+KHTHxFFLH51SfaGOoUdW+v7ViQ==" - }, - "node_modules/hast-util-to-jsx-runtime/node_modules/style-to-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.5.tgz", - "integrity": "sha512-rDRwHtoDD3UMMrmZ6BzOW0naTjMsVZLIjsGleSKS/0Oz+cgCfAPRspaqJuE8rDzpKha/nEvnM0IF4seEAZUTKQ==", - "dependencies": { - "inline-style-parser": "0.2.2" - } - }, - "node_modules/hast-util-to-parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", - "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", - "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/headers-polyfill": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", - "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==" - }, - "node_modules/hls.js": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.4.14.tgz", - "integrity": "sha512-UppQjyvPVclg+6t2KY/Rv03h0+bA5u6zwqVoz4LAC/L0fgYmIaCD7ZCrwe8WI1Gv01be1XL0QFsRbSdIHV/Wbw==" - }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "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/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "engines": { - "node": ">=16.17.0" - } - }, - "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/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", - "engines": { - "node": ">= 4" - } - }, - "node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/inline-style-parser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==" - }, - "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "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-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "engines": { - "node": ">=4" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "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/is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dependencies": { - "call-bind": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", - "dependencies": { - "which-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", - "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" - }, - "node_modules/isbot": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.2.tgz", - "integrity": "sha512-Yk2X9QhmhzyxKx4JYfeanqxODxDc2CoU38/uymjkvW/CYww6GPH8e65sOuLz0SIkFjW6pCg/iM6vLdohzA4WOQ==", - "engines": { - "node": ">=18" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" - }, - "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jose": { - "version": "4.15.5", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", - "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lodash.escape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", - "integrity": "sha512-nXEOnb/jK9g0DYMr1/Xvq6l5xMD7GDG55+GSYIYmS0G4tBk/hURD4JR9WCavs04t33WmJx9kCyp9vJ+mr4BOUw==" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/loupe": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.0.tgz", - "integrity": "sha512-qKl+FrLXUhFuHUoDJG7f8P8gEMHq9NFS0c6ghXG1J0rldmZFQZoNVv/vyirE9qwCIhWZDsvEFd1sbFu3GvRQFg==", - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "bin": { - "lz-string": "bin/bin.js" - } - }, - "node_modules/markdown-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", - "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdown-table": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", - "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/md5-hex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-5.0.0.tgz", - "integrity": "sha512-18TKd0nxBzMLflLBSCM/I9n50izl7NQGuujgbKjVUs/9acY+a5uzpDUVd4wV130vaK67TzDnPin2gze88u+e4Q==", - "dependencies": { - "blueimp-md5": "^2.19.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", - "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz", - "integrity": "sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-frontmatter": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", - "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "escape-string-regexp": "^5.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-extension-frontmatter": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", - "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz", - "integrity": "sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==", - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", - "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", - "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz", - "integrity": "sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.2.tgz", - "integrity": "sha512-eKMQDeywY2wlHc97k5eD8VC+9ASMjN8ItEZQNGwJ6E0XWKiW/Z0V5/H8pvoXUf+y+Mj0VIgeRRbujBmFn4FTyA==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-remove-position": "^5.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz", - "integrity": "sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", - "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdx-bundler": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/mdx-bundler/-/mdx-bundler-10.0.1.tgz", - "integrity": "sha512-y/+RtkJf+vTQTl8Ae/zql42igpcp44ja8a2S74tWq87rX+R22Zwn4OmPc5APO+xhIaIy/I1oxoxlPrt4GwEuGQ==", - "dependencies": { - "@babel/runtime": "^7.23.2", - "@esbuild-plugins/node-resolve": "^0.2.2", - "@fal-works/esbuild-plugin-global-externals": "^2.1.2", - "@mdx-js/esbuild": "^3.0.0", - "gray-matter": "^4.0.3", - "remark-frontmatter": "^5.0.0", - "remark-mdx-frontmatter": "^4.0.0", - "uuid": "^9.0.1", - "vfile": "^6.0.1" - }, - "engines": { - "node": ">=18", - "npm": ">=6" - }, - "peerDependencies": { - "esbuild": "0.*" - } - }, - "node_modules/media-chrome": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-3.1.1.tgz", - "integrity": "sha512-EqofPNX7Eq1dv9ixuRo51Wv/Wo3P4PIPjY4iM1jGqu/Jyond8BqdigQKdhxPM2rH9SohdUXZvUWS1OJ1xMO6Ww==" - }, - "node_modules/media-tracks": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-tracks/-/media-tracks-0.3.0.tgz", - "integrity": "sha512-kicD8eOFwe6nD7jn7iM/0yuLzWuo6abWHURYwY7NhxL1dBif+lt0on4rLTs6VhKwAEE/BjT3wr+0vn1w8SBpag==" - }, - "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/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "engines": { - "node": ">= 8" - } - }, - "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/micromark": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", - "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz", - "integrity": "sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-frontmatter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", - "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", - "dependencies": { - "fault": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.0.0.tgz", - "integrity": "sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg==", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.0.0.tgz", - "integrity": "sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg==", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw==", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz", - "integrity": "sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw==", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.0.1.tgz", - "integrity": "sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw==", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-expression": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.0.tgz", - "integrity": "sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-jsx": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.0.tgz", - "integrity": "sha512-uvhhss8OGuzR4/N17L1JwvmJIpPhAd8oByMawEKx6NVdBCbesjH4t+vjEp3ZXft9DwvlKSD07fCeI44/N0Vf2w==", - "dependencies": { - "@types/acorn": "^4.0.0", - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-md": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", - "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", - "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", - "dependencies": { - "acorn": "^8.0.0", - "acorn-jsx": "^5.0.0", - "micromark-extension-mdx-expression": "^3.0.0", - "micromark-extension-mdx-jsx": "^3.0.0", - "micromark-extension-mdx-md": "^2.0.0", - "micromark-extension-mdxjs-esm": "^3.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs-esm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", - "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", - "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", - "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-mdx-expression": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.1.tgz", - "integrity": "sha512-F0ccWIUHRLRrYp5TC9ZYXmZo+p2AM13ggbsW4T0b5CRKP8KHVRB8t4pwtBgTxtjRmwrK0Irwm7vs2JOZabHZfg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", - "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", - "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", - "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", - "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", - "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", - "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", - "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", - "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", - "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-events-to-acorn": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.2.tgz", - "integrity": "sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/acorn": "^4.0.0", - "@types/estree": "^1.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "estree-util-visit": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - } - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", - "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", - "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", - "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", - "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz", - "integrity": "sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", - "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/micromark/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.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/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/morgan": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", - "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", - "dependencies": { - "basic-auth": "~2.0.1", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-finished": "~2.3.0", - "on-headers": "~1.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/morgan/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/msw": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.2.10.tgz", - "integrity": "sha512-OQhHBocUsI8j+czCTRouGCGYE8pk6hq8HQ0HFg9mYQg7KCzqVpUSbMikmRbRXGoid28FFvYqjbxB3/UWw50VZQ==", - "hasInstallScript": true, - "dependencies": { - "@bundled-es-modules/cookie": "^2.0.0", - "@bundled-es-modules/statuses": "^1.0.1", - "@inquirer/confirm": "^3.0.0", - "@mswjs/cookies": "^1.1.0", - "@mswjs/interceptors": "^0.25.16", - "@open-draft/until": "^2.1.0", - "@types/cookie": "^0.6.0", - "@types/statuses": "^2.0.4", - "chalk": "^4.1.2", - "graphql": "^16.8.1", - "headers-polyfill": "^4.0.2", - "is-node-process": "^1.2.0", - "outvariant": "^1.4.2", - "path-to-regexp": "^6.2.0", - "strict-event-emitter": "^0.5.1", - "type-fest": "^4.9.0", - "yargs": "^17.7.2" - }, - "bin": { - "msw": "cli/index.js" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mswjs" - }, - "peerDependencies": { - "typescript": ">= 4.7.x" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/msw/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/msw/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/msw/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/msw/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/msw/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/msw/node_modules/path-to-regexp": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", - "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" - }, - "node_modules/msw/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/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/mux-embed": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/mux-embed/-/mux-embed-4.30.0.tgz", - "integrity": "sha512-XAgAp4CEvsiZL26GbruzeG1g33OWyrzuskDMavXUxDufTxS0/AxAhwoTTRqEzEJS9vnZa/X9R2GV3xRX1XMp2w==" - }, - "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/node-emoji": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", - "integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==", - "dependencies": { - "@sindresorhus/is": "^4.6.0", - "char-regex": "^1.0.2", - "emojilib": "^2.4.0", - "skin-tone": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", - "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", - "engines": { - "node": ">= 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/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", - "dependencies": { - "call-bind": "^1.0.5", - "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/oidc-token-hash": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", - "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", - "engines": { - "node": "^10.13.0 || >=12.0.0" - } - }, - "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/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/openid-client": { - "version": "5.6.5", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", - "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==", - "dependencies": { - "jose": "^4.15.5", - "lru-cache": "^6.0.0", - "object-hash": "^2.2.0", - "oidc-token-hash": "^5.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/panva" - } - }, - "node_modules/openid-client/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/outvariant": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.2.tgz", - "integrity": "sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==" - }, - "node_modules/p-queue": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.0.1.tgz", - "integrity": "sha512-NXzu9aQJTAzbBqOt2hwsR63ea7yvxJc0PwN/zobNAudYfb1B7R08SzB4TsLeSbUCuG467NhnoT0oO6w1qRO+BA==", - "dependencies": { - "eventemitter3": "^5.0.1", - "p-timeout": "^6.1.2" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.2.tgz", - "integrity": "sha512-UbD77BuZ9Bc9aABo74gfXhNvzC9Tx7SxtHSh1fxvx3jTLLYvmVhiQZZrJzqqU0jKbN32kb5VOKiLEQI/3bIjgQ==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-entities": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", - "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", - "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" - }, - "node_modules/parse-git-diff": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/parse-git-diff/-/parse-git-diff-0.0.15.tgz", - "integrity": "sha512-KU7FMvw7ybx2BqkSJthyLl6G4LmKzWwOyalNLdJtzkV7xIFU5oCIVDP9OQl+0vkVv7EKiKDnJlkdzPQ6ueFsgw==" - }, - "node_modules/parse-numeric-range": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", - "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==" - }, - "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "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/partysocket": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-1.0.1.tgz", - "integrity": "sha512-sSnLf9X0Oaxw0wXp0liKho0QQqStDJB5I4ViaqmtI4nHm6cpb2kUealErPrcQpYUF6zgTHzLQhIO++2tcJc59A==", - "dependencies": { - "event-target-shim": "^6.0.2" - } - }, - "node_modules/partysocket/node_modules/event-target-shim": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz", - "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==", - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "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-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", - "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "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/path-type": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", - "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/periscopic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", - "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^3.0.0", - "is-reference": "^3.0.0" - } - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pid-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pid-port/-/pid-port-1.0.0.tgz", - "integrity": "sha512-LSNBeKChRPA4Xlrs6+zV588G1hSrFvANtPV5rt/5MPfSPK3V9XPWxx1d29svsrOjngT9ifLisXWCLS7DvO9ZhQ==", - "dependencies": { - "execa": "^8.0.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/playwright": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", - "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", - "dependencies": { - "playwright-core": "1.42.1" - }, - "bin": { - "playwright": "cli.js" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "fsevents": "2.3.2" - } - }, - "node_modules/playwright-core": { - "version": "1.42.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", - "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", - "bin": { - "playwright-core": "cli.js" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/playwright/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/process-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-exists/-/process-exists-5.0.0.tgz", - "integrity": "sha512-6QPRh5fyHD8MaXr4GYML8K/YY0Sq5dKHGIOrAKS3cYpHQdmygFCcijIu1dVoNKAZ0TWAMoeh8KDK9dF8auBkJA==", - "dependencies": { - "ps-list": "^8.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/prop-types/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, - "node_modules/property-information": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.4.1.tgz", - "integrity": "sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "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/ps-list": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/ps-list/-/ps-list-8.1.1.tgz", - "integrity": "sha512-OPS9kEJYVmiO48u/B9qneqhkMvgCxT+Tm28VCEJpheTpl8cJ0ffZRRNgS5mrQRTrX5yRTpaJ+hRDeefXYmmorQ==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "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/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": "18.2.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", - "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - }, - "node_modules/react-remove-scroll": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", - "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", - "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", - "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", - "dependencies": { - "react-style-singleton": "^2.2.1", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", - "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", - "dependencies": { - "get-nonce": "^1.0.0", - "invariant": "^2.2.4", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "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/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", - "dependencies": { - "call-bind": "^1.0.6", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/rehype": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.1.tgz", - "integrity": "sha512-AcSLS2mItY+0fYu9xKxOu1LhUZeBZZBx8//5HKzF+0XP+eP8+6a5MXn2+DW2kfXR6Dtp1FEXMVrjyKAcvcU8vg==", - "dependencies": { - "@types/hast": "^3.0.0", - "rehype-parse": "^9.0.0", - "rehype-stringify": "^10.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-parse": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.0.tgz", - "integrity": "sha512-WG7nfvmWWkCR++KEkZevZb/uw41E8TsH4DsY9UxsTbIXCVGbAs4S+r8FrQ+OtH5EEQAs+5UxKC42VinkmpA1Yw==", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-from-html": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-stringify": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.0.tgz", - "integrity": "sha512-1TX1i048LooI9QoecrXy7nGFFbFSufxVRAfc6Y9YMRAi56l+oB0zP51mLSV312uRuvVLPV1opSlJmslozR1XHQ==", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-to-html": "^9.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", - "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", - "dependencies": { - "@types/mdast": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-autolink-headings": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/remark-autolink-headings/-/remark-autolink-headings-7.0.1.tgz", - "integrity": "sha512-a1BIwoJ0cSnX+sPp5u3AFULBFWHGYBt57Fo4a+7IlGiJOQxs8b7uYAE5Iu26Ocl7Y5cvinZy3FaGVruLCKg6vA==", - "dependencies": { - "@types/hast": "^2.0.0", - "@types/mdast": "^3.0.0", - "extend": "^3.0.0", - "unified": "^10.0.0", - "unist-util-visit": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-autolink-headings/node_modules/@types/hast": { - "version": "2.3.10", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", - "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-autolink-headings/node_modules/@types/mdast": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", - "integrity": "sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==", - "dependencies": { - "@types/unist": "^2" - } - }, - "node_modules/remark-autolink-headings/node_modules/@types/unist": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", - "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" - }, - "node_modules/remark-autolink-headings/node_modules/unified": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz", - "integrity": "sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==", - "dependencies": { - "@types/unist": "^2.0.0", - "bail": "^2.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-autolink-headings/node_modules/unist-util-is": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz", - "integrity": "sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-autolink-headings/node_modules/unist-util-stringify-position": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz", - "integrity": "sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==", - "dependencies": { - "@types/unist": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-autolink-headings/node_modules/unist-util-visit": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz", - "integrity": "sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0", - "unist-util-visit-parents": "^5.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-autolink-headings/node_modules/unist-util-visit-parents": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz", - "integrity": "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-is": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-autolink-headings/node_modules/vfile": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", - "integrity": "sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==", - "dependencies": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^3.0.0", - "vfile-message": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-autolink-headings/node_modules/vfile-message": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz", - "integrity": "sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==", - "dependencies": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-emoji": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-4.0.1.tgz", - "integrity": "sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==", - "dependencies": { - "@types/mdast": "^4.0.2", - "emoticon": "^4.0.1", - "mdast-util-find-and-replace": "^3.0.1", - "node-emoji": "^2.1.0", - "unified": "^11.0.4" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/remark-frontmatter": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", - "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-frontmatter": "^2.0.0", - "micromark-extension-frontmatter": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-gfm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", - "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.0.1.tgz", - "integrity": "sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA==", - "dependencies": { - "mdast-util-mdx": "^3.0.0", - "micromark-extension-mdxjs": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx-frontmatter": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/remark-mdx-frontmatter/-/remark-mdx-frontmatter-4.0.0.tgz", - "integrity": "sha512-PZzAiDGOEfv1Ua7exQ8S5kKxkD8CDaSb4nM+1Mprs6u8dyvQifakh+kCj6NovfGXW+bTvrhjaR3srzjS2qJHKg==", - "dependencies": { - "@types/mdast": "^4.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-util-value-to-estree": "^3.0.0", - "toml": "^3.0.0", - "unified": "^11.0.0", - "yaml": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/remcohaszing" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", - "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remix-flat-routes": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/remix-flat-routes/-/remix-flat-routes-0.6.4.tgz", - "integrity": "sha512-/0bTfaNSd2O3ak+cCLAlmHIsgm7YWiaBiu5ENUyuSWSNwF/KaHuKFhcHHhozPevQ8ROgCZG9tNbWzH6ktJoprA==", - "dependencies": { - "@remix-run/v1-route-convention": "^0.1.3", - "fs-extra": "^11.1.1", - "minimatch": "^5.1.0" - }, - "bin": { - "migrate-flat-routes": "dist/cli.js" - }, - "peerDependencies": { - "@remix-run/dev": "^1.15.0 || ^2" - } - }, - "node_modules/remix-flat-routes/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "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/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/run-async": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", - "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "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.23.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dependencies": { - "loose-envify": "^1.1.0" - } - }, - "node_modules/section-matter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", - "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", - "dependencies": { - "extend-shallow": "^2.0.1", - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "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-cookie-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", - "integrity": "sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "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/shiki": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.2.0.tgz", - "integrity": "sha512-xLhiTMOIUXCv5DqJ4I70GgQCtdlzsTqFLZWcMHHG3TAieBUbvEGthdrlPDlX4mL/Wszx9C6rEcxU6kMlg4YlxA==", - "dependencies": { - "@shikijs/core": "1.2.0" - } - }, - "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "dependencies": { - "call-bind": "^1.0.7", - "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/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/skin-tone": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", - "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", - "dependencies": { - "unicode-emoji-modifier-base": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sonner": { - "version": "1.4.41", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.4.41.tgz", - "integrity": "sha512-uG511ggnnsw6gcn/X+YKkWPo5ep9il9wYi3QJxHsYe7yTZ4+cOd1wuodOUmOpFuXL+/RE3R04LczdNCDygTDgQ==", - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, - "node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "engines": { - "node": ">= 8" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/spin-delay": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/spin-delay/-/spin-delay-1.2.0.tgz", - "integrity": "sha512-PkZl5FHWOMrwQgoWejG1hBkIlVx4KbdL/37RPr5/pGq5+NWcGx7NNDukFct2yr8yRZuvwEompNR/in9nWj4sTw==", - "peerDependencies": { - "react": ">=17.0.1" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "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/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/stream-slice": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/stream-slice/-/stream-slice-0.1.2.tgz", - "integrity": "sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==" - }, - "node_modules/strict-event-emitter": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", - "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==" - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "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/string-width-cjs/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/string-width-cjs/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/stringify-entities": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz", - "integrity": "sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g==", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "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/strip-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/strip-bom-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-to-object": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz", - "integrity": "sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==", - "dependencies": { - "inline-style-parser": "0.1.1" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tailwindcss-radix": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tailwindcss-radix/-/tailwindcss-radix-2.9.0.tgz", - "integrity": "sha512-N49SnciSeRgLC+VK+Fu5VULNJIvJUvN7tUKF1kEHPXrS76WAlwrSVthCbJ9NUw0Cj/ptxs73pdVEdosomAN5Lg==" - }, - "node_modules/taskkill": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/taskkill/-/taskkill-5.0.0.tgz", - "integrity": "sha512-+HRtZ40Vc+6YfCDWCeAsixwxJgMbPY4HHuTgzPYH3JXvqHWUlsCfy+ylXlAKhFNcuLp4xVeWeFBUhDk+7KYUvQ==", - "dependencies": { - "execa": "^6.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/taskkill/node_modules/execa": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-6.1.0.tgz", - "integrity": "sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^3.0.1", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/taskkill/node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/taskkill/node_modules/human-signals": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-3.0.1.tgz", - "integrity": "sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==", - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/taskkill/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, - "node_modules/tinypool": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz", - "integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "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/toml": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "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/tween-functions": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz", - "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==" - }, - "node_modules/type-fest": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.13.1.tgz", - "integrity": "sha512-ASMgM+Vf2cLwDMt1KXSkMUDSYCxtckDJs8zsaVF/mYteIsiARKCVtyXtcK38mIKbLTctZP8v6GMqdNaeI3fo7g==", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, - "node_modules/unicode-emoji-modifier-base": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", - "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unified": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz", - "integrity": "sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ==", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position-from-estree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", - "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-remove-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", - "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-visit": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "engines": { - "node": ">= 10.0.0" - } - }, - "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/use-callback-ref": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", - "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", - "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "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/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "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/vfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", - "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.2.tgz", - "integrity": "sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg==", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/web-encoding": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", - "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", - "dependencies": { - "util": "^0.12.3" - }, - "optionalDependencies": { - "@zxing/text-encoding": "0.9.0" - } - }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "engines": { - "node": ">= 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/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi-cjs/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/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "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/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/yaml": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", - "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "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" - } - }, - "node_modules/yargs/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/yargs/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/yargs/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/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/kcdshop/package.json b/kcdshop/package.json deleted file mode 100644 index 99a3bde..0000000 --- a/kcdshop/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "module", - "scripts": { - "start": "cd .. && npm start" - }, - "dependencies": { - "@kentcdodds/workshop-app": "^3.13.0", - "@kentcdodds/workshop-utils": "^3.13.0" - } -} diff --git a/kcdshop/setup-custom.js b/kcdshop/setup-custom.js deleted file mode 100644 index 9728cf4..0000000 --- a/kcdshop/setup-custom.js +++ /dev/null @@ -1,37 +0,0 @@ -import path from 'node:path' -import { - getApps, - isProblemApp, - setPlayground, -} from '@kentcdodds/workshop-utils/apps.server' -import { getWatcher } from '@kentcdodds/workshop-utils/change-tracker.server' -import fsExtra from 'fs-extra' - -// getApps expects this env var -process.env.NODE_ENV = 'development' - -const allApps = await getApps() -const problemApps = allApps.filter(isProblemApp) - -if (!process.env.SKIP_PLAYGROUND) { - const firstProblemApp = problemApps[0] - if (firstProblemApp) { - console.log('๐Ÿ› setting up the first problem app...') - const playgroundPath = path.join(process.cwd(), 'playground') - if (await fsExtra.exists(playgroundPath)) { - console.log('๐Ÿ—‘ deleting existing playground app') - await fsExtra.remove(playgroundPath) - } - await setPlayground(firstProblemApp.fullPath).then( - () => { - console.log('โœ… first problem app set up') - }, - error => { - console.error(error) - throw new Error('โŒ first problem app setup failed') - }, - ) - } -} - -getWatcher().close() diff --git a/kcdshop/test.js b/kcdshop/test.js deleted file mode 100644 index 3591075..0000000 --- a/kcdshop/test.js +++ /dev/null @@ -1,75 +0,0 @@ -// This should run by node without any dependencies -// because you may need to run it without deps. - -import { spawn } from 'child_process' -import path from 'node:path' -import { - getApps, - isExampleApp, - isSolutionApp, -} from '@kentcdodds/workshop-utils/apps.server' - -const styles = { - // got these from playing around with what I found from: - // https://github.com/istanbuljs/istanbuljs/blob/0f328fd0896417ccb2085f4b7888dd8e167ba3fa/packages/istanbul-lib-report/lib/file-writer.js#L84-L96 - // they're the best I could find that works well for light or dark terminals - success: { open: '\u001b[32;1m', close: '\u001b[0m' }, - danger: { open: '\u001b[31;1m', close: '\u001b[0m' }, - info: { open: '\u001b[36;1m', close: '\u001b[0m' }, - subtitle: { open: '\u001b[2;1m', close: '\u001b[0m' }, -} -function color(modifier, string) { - return styles[modifier].open + string + styles[modifier].close -} - -const __dirname = new URL('.', import.meta.url).pathname -const here = (...p) => path.join(__dirname, ...p) - -const workshopRoot = here('..') - -const relativeToWorkshopRoot = dir => - dir.replace(`${workshopRoot}${path.sep}`, '') - -// bundleMDX - throw when process.NODE_ENV is not a string -// @kentcdodds/workshop-app/build/utils/compile-mdx.server -process.env.NODE_ENV = 'development' - -const apps = await getApps() -const solutionApps = apps.filter(isSolutionApp) -const exampleApps = apps.filter(isExampleApp) - -let exitCode = 0 - -for (const app of [...solutionApps, ...exampleApps]) { - if (app.test.type !== 'script') continue - - const relativePath = relativeToWorkshopRoot(app.fullPath) - - console.log(`๐Ÿงช Running "${app.test.script}" in ${relativePath}`) - - const cp = spawn('npm', ['run', app.test.script, '--silent'], { - cwd: app.fullPath, - stdio: 'inherit', - shell: true, - windowsHide: false, - env: { - OPEN_PLAYWRIGHT_REPORT: 'never', - ...process.env, - PORT: app.dev.portNumber, - }, - }) - - await new Promise(res => { - cp.on('exit', code => { - if (code === 0) { - console.log(color('success', `โœ… Tests passed (${relativePath})`)) - } else { - exitCode = 1 - console.error(color('danger', `โŒ Tests failed (${relativePath})`)) - } - res() - }) - }) -} - -process.exit(exitCode) diff --git a/kcdshop/update-deps.sh b/kcdshop/update-deps.sh deleted file mode 100755 index c92db27..0000000 --- a/kcdshop/update-deps.sh +++ /dev/null @@ -1,6 +0,0 @@ -npx npm-check-updates --dep prod,dev --upgrade --workspaces --root -rm -rf node_modules package-lock.json ./exercises/**/node_modules -npm install -node setup.js -npm run typecheck -npm run lint --fix diff --git a/package-lock.json b/package-lock.json index 933d803..1fd9a0f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,731 +11,685 @@ "exercises/*/*", "examples/*" ], - "dependencies": { - "eslint": "^8.57.0", - "eslint-plugin-import": "^2.29.1" - }, "devDependencies": { - "execa": "^8.0.1", - "fs-extra": "^11.1.1", - "prettier": "^3.2.5" + "@epic-web/config": "^1.18.2", + "eslint": "^9.23.0", + "prettier": "^3.5.3" }, "engines": { "git": ">=2.18.0", - "node": ">=18", + "node": ">=20", "npm": ">=8.16.0" } }, - "exercises/01.exercises/01.problem.ssr": { - "name": "exercises__sep__01.exercises__sep__01.problem.ssr", + "exercises/01.init/01.problem.static": { + "name": "exercises__sep__01.init__sep__01.problem.static", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" - } - }, - "exercises/01.exercises/01.problem.ssr/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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/01.solution.ssr": { - "name": "exercises__sep__01.exercises__sep__01.solution.ssr", + "exercises/01.init/01.solution.static": { + "name": "exercises__sep__01.init__sep__01.solution.static", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" - } - }, - "exercises/01.exercises/01.solution.ssr/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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/02.problem.server-context": { - "name": "exercises__sep__01.exercises__sep__02.problem.server-context", + "exercises/02.server-components/01.problem.rsc": { + "name": "exercises__sep__02.server-components__sep__01.problem.rsc", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" - } - }, - "exercises/01.exercises/02.problem.server-context/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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/02.solution.server-context": { - "name": "exercises__sep__01.exercises__sep__02.solution.server-context", + "exercises/02.server-components/01.solution.rsc": { + "name": "exercises__sep__02.server-components__sep__01.solution.rsc", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/02.solution.server-context/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" + "exercises/02.server-components/02.problem.async-components": { + "name": "exercises__sep__02.server-components__sep__02.problem.async-components", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/03.problem.url": { - "name": "exercises__sep__01.exercises__sep__03.problem.url", + "exercises/02.server-components/02.solution.async-components": { + "name": "exercises__sep__02.server-components__sep__02.solution.async-components", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/03.problem.url/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" + "exercises/02.server-components/03.problem.streaming": { + "name": "exercises__sep__02.server-components__sep__03.problem.streaming", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/03.solution.url": { - "name": "exercises__sep__01.exercises__sep__03.solution.url", + "exercises/02.server-components/03.solution.streaming": { + "name": "exercises__sep__02.server-components__sep__03.solution.streaming", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/03.solution.url/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" + "exercises/02.server-components/04.problem.server-context": { + "name": "exercises__sep__02.server-components__sep__04.problem.server-context", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/04.problem.async-components": { - "name": "exercises__sep__01.exercises__sep__04.problem.async-components", + "exercises/02.server-components/04.solution.server-context": { + "name": "exercises__sep__02.server-components__sep__04.solution.server-context", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/04.problem.async-components/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" + "exercises/03.client-components/01.problem.loader": { + "name": "exercises__sep__03.client-components__sep__01.problem.loader", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/04.solution.async-components": { - "name": "exercises__sep__01.exercises__sep__04.solution.async-components", + "exercises/03.client-components/01.solution.loader": { + "name": "exercises__sep__03.client-components__sep__01.solution.loader", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/04.solution.async-components/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" + "exercises/03.client-components/02.problem.module-resolution": { + "name": "exercises__sep__03.client-components__sep__02.problem.module-resolution", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/05.problem.bootstrap": { - "name": "exercises__sep__01.exercises__sep__05.problem.bootstrap", + "exercises/03.client-components/02.solution.module-resolution": { + "name": "exercises__sep__03.client-components__sep__02.solution.module-resolution", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/05.problem.bootstrap/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" + "exercises/04.router/01.problem.router": { + "name": "exercises__sep__04.router__sep__01.problem.router", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/05.solution.bootstrap": { - "name": "exercises__sep__01.exercises__sep__05.solution.bootstrap", + "exercises/04.router/01.solution.router": { + "name": "exercises__sep__04.router__sep__01.solution.router", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/05.solution.bootstrap/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" + "exercises/04.router/02.problem.pending-ui": { + "name": "exercises__sep__04.router__sep__02.problem.pending-ui", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/06.problem.import-map": { - "name": "exercises__sep__01.exercises__sep__06.problem.import-map", + "exercises/04.router/02.solution.pending-ui": { + "name": "exercises__sep__04.router__sep__02.solution.pending-ui", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/06.problem.import-map/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" + "exercises/04.router/03.problem.race-conditions": { + "name": "exercises__sep__04.router__sep__03.problem.race-conditions", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/06.solution.import-map": { - "name": "exercises__sep__01.exercises__sep__06.solution.import-map", + "exercises/04.router/03.solution.race-conditions": { + "name": "exercises__sep__04.router__sep__03.solution.race-conditions", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/06.solution.import-map/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" + "exercises/04.router/04.problem.history": { + "name": "exercises__sep__04.router__sep__04.problem.history", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/07.problem.module-graph": { - "name": "exercises__sep__01.exercises__sep__07.problem.module-graph", + "exercises/04.router/04.solution.history": { + "name": "exercises__sep__04.router__sep__04.solution.history", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/07.problem.module-graph/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" + "exercises/04.router/05.problem.cache": { + "name": "exercises__sep__04.router__sep__05.problem.cache", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/07.solution.module-graph": { - "name": "exercises__sep__01.exercises__sep__07.solution.module-graph", + "exercises/04.router/05.solution.cache": { + "name": "exercises__sep__04.router__sep__05.solution.cache", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/07.solution.module-graph/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" + "exercises/05.actions/01.problem.action-reference": { + "name": "exercises__sep__05.actions__sep__01.problem.action-reference", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/08.problem.hydrate": { - "name": "exercises__sep__01.exercises__sep__08.problem.hydrate", + "exercises/05.actions/01.solution.action-reference": { + "name": "exercises__sep__05.actions__sep__01.solution.action-reference", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/08.problem.hydrate/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" + "exercises/05.actions/02.problem.client": { + "name": "exercises__sep__05.actions__sep__02.problem.client", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/08.solution.hydrate": { - "name": "exercises__sep__01.exercises__sep__08.solution.hydrate", + "exercises/05.actions/02.solution.client": { + "name": "exercises__sep__05.actions__sep__02.solution.client", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/08.solution.hydrate/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" + "exercises/05.actions/03.problem.server": { + "name": "exercises__sep__05.actions__sep__03.problem.server", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/09.problem.routing": { - "name": "exercises__sep__01.exercises__sep__09.problem.routing", + "exercises/05.actions/03.solution.server": { + "name": "exercises__sep__05.actions__sep__03.solution.server", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/09.problem.routing/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" + "exercises/05.actions/04.problem.revalidation": { + "name": "exercises__sep__05.actions__sep__04.problem.revalidation", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/09.solution.routing": { - "name": "exercises__sep__01.exercises__sep__09.solution.routing", + "exercises/05.actions/04.solution.revalidation": { + "name": "exercises__sep__05.actions__sep__04.solution.revalidation", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/09.solution.routing/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" + "exercises/05.actions/05.problem.history-revalidation": { + "name": "exercises__sep__05.actions__sep__05.problem.history-revalidation", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", + "close-with-grace": "^1.3.0", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-error-boundary": "^4.0.13", + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/10.problem.actions": { - "name": "exercises__sep__01.exercises__sep__10.problem.actions", + "exercises/05.actions/05.solution.history-revalidation": { + "name": "exercises__sep__05.actions__sep__05.solution.history-revalidation", "version": "1.0.0", "license": "MIT", "dependencies": { - "body-parser": "^1.20.2", - "busboy": "^1.6.0", - "chalk": "^5.3.0", + "@hono/node-server": "^1.11.1", + "@playwright/test": "^1.47.1", "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", + "hono": "^4.3.7", + "react": "^19.0.0", + "react-dom": "^19.0.0", "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" + "react-server-dom-esm": "npm:@kentcdodds/tmp-react-server-dom-esm@19.0.1" } }, - "exercises/01.exercises/10.problem.actions/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" + "node_modules/@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "engines": { + "node": ">=6.9.0" } }, - "exercises/01.exercises/10.solution.actions": { - "name": "exercises__sep__01.exercises__sep__10.solution.actions", - "version": "1.0.0", + "node_modules/@emnapi/core": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz", + "integrity": "sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==", + "dev": true, "license": "MIT", + "optional": true, "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" + "@emnapi/wasi-threads": "1.0.1", + "tslib": "^2.4.0" } }, - "exercises/01.exercises/10.solution.actions/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/@emnapi/runtime": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", + "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "engines": { - "node": ">=0.10.0" + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz", + "integrity": "sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@babel/runtime": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", - "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", + "node_modules/@epic-web/config": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/@epic-web/config/-/config-1.18.2.tgz", + "integrity": "sha512-Q4sfcCxVaI2ojrNO/ldxrVd03ZL283BtZJLvJvKTgQgABU/vuDJyvlGaWMlHax+ZQnLFl4X6dKRdGJc+nyY3Wg==", + "dev": true, + "license": "MIT", "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" + "@total-typescript/ts-reset": "^0.6.1", + "@vitest/eslint-plugin": "^1.1.14", + "eslint-plugin-import-x": "^4.5.0", + "eslint-plugin-jest-dom": "^5.5.0", + "eslint-plugin-playwright": "^2.2.0", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-testing-library": "^7.1.0", + "globals": "^15.13.0", + "prettier-plugin-tailwindcss": "^0.6.9", + "tslib": "^2.8.1", + "typescript-eslint": "^8.17.0" } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", + "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz", + "integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -743,37 +697,115 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", + "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", + "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@eslint/core": "^0.12.0", + "levn": "^0.4.1" }, "engines": { - "node": ">=10.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@hono/node-server": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.14.0.tgz", + "integrity": "sha512-YUCxJwgHRKSqjrdTk9e4VMGKN27MK5r4+MGPyZTgKH+IYbK+KtYbHeOcPGJ91KGGD6RIQiz2dAHxvjauNhOS8g==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -782,78 +814,514 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.7.tgz", + "integrity": "sha512-5yximcFK5FNompXfJFoWanu5l8v1hNGqNHh9du1xETp9HWk/B/PzvchX55WYOPaIeNglG8++68AAiauBAtbnzw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.3.1", + "@emnapi/runtime": "^1.3.1", + "@tybys/wasm-util": "^0.9.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz", + "integrity": "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@total-typescript/ts-reset": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@total-typescript/ts-reset/-/ts-reset-0.6.1.tgz", + "integrity": "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", + "integrity": "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/doctrine": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz", + "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/type-utils": "8.28.0", + "@typescript-eslint/utils": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz", + "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz", + "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz", + "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.28.0", + "@typescript-eslint/utils": "8.28.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz", + "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz", + "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/visitor-keys": "8.28.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz", + "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.28.0", + "@typescript-eslint/types": "8.28.0", + "@typescript-eslint/typescript-estree": "8.28.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz", + "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.28.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/rspack-resolver-binding-darwin-arm64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-darwin-arm64/-/rspack-resolver-binding-darwin-arm64-1.2.2.tgz", + "integrity": "sha512-i7z0B+C0P8Q63O/5PXJAzeFtA1ttY3OR2VSJgGv18S+PFNwD98xHgAgPOT1H5HIV6jlQP8Avzbp09qxJUdpPNw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-darwin-x64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-darwin-x64/-/rspack-resolver-binding-darwin-x64-1.2.2.tgz", + "integrity": "sha512-YEdFzPjIbDUCfmehC6eS+AdJYtFWY35YYgWUnqqTM2oe/N58GhNy5yRllxYhxwJ9GcfHoNc6Ubze1yjkNv+9Qg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-freebsd-x64": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-freebsd-x64/-/rspack-resolver-binding-freebsd-x64-1.2.2.tgz", + "integrity": "sha512-TU4ntNXDgPN2giQyyzSnGWf/dVCem5lvwxg0XYvsvz35h5H19WrhTmHgbrULMuypCB3aHe1enYUC9rPLDw45mA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-arm-gnueabihf": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-arm-gnueabihf/-/rspack-resolver-binding-linux-arm-gnueabihf-1.2.2.tgz", + "integrity": "sha512-ik3w4/rU6RujBvNWiDnKdXi1smBhqxEDhccNi/j2rHaMjm0Fk49KkJ6XKsoUnD2kZ5xaMJf9JjailW/okfUPIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-arm64-gnu": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-arm64-gnu/-/rspack-resolver-binding-linux-arm64-gnu-1.2.2.tgz", + "integrity": "sha512-fp4Azi8kHz6TX8SFmKfyScZrMLfp++uRm2srpqRjsRZIIBzH74NtSkdEUHImR4G7f7XJ+sVZjCc6KDDK04YEpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/rspack-resolver-binding-linux-arm64-musl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-arm64-musl/-/rspack-resolver-binding-linux-arm64-musl-1.2.2.tgz", + "integrity": "sha512-gMiG3DCFioJxdGBzhlL86KcFgt9HGz0iDhw0YVYPsShItpN5pqIkNrI+L/Q/0gfDiGrfcE0X3VANSYIPmqEAlQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } + "node_modules/@unrs/rspack-resolver-binding-linux-x64-gnu": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-x64-gnu/-/rspack-resolver-binding-linux-x64-gnu-1.2.2.tgz", + "integrity": "sha512-n/4n2CxaUF9tcaJxEaZm+lqvaw2gflfWQ1R9I7WQgYkKEKbRKbpG/R3hopYdUmLSRI4xaW1Cy0Bz40eS2Yi4Sw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "engines": { - "node": ">= 8" - } + "node_modules/@unrs/rspack-resolver-binding-linux-x64-musl": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-linux-x64-musl/-/rspack-resolver-binding-linux-x64-musl-1.2.2.tgz", + "integrity": "sha512-cHyhAr6rlYYbon1L2Ag449YCj3p6XMfcYTP0AQX+KkQo025d1y/VFtPWvjMhuEsE2lLvtHm7GdJozj6BOMtzVg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@unrs/rspack-resolver-binding-wasm32-wasi": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-wasm32-wasi/-/rspack-resolver-binding-wasm32-wasi-1.2.2.tgz", + "integrity": "sha512-eogDKuICghDLGc32FtP+WniG38IB1RcGOGz0G3z8406dUdjJvxfHGuGs/dSlM9YEp/v0lEqhJ4mBu6X2nL9pog==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@napi-rs/wasm-runtime": "^0.2.7" }, "engines": { - "node": ">= 8" + "node": ">=14.0.0" } }, - "node_modules/@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" - }, - "node_modules/@types/node": { - "version": "20.11.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", - "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", + "node_modules/@unrs/rspack-resolver-binding-win32-arm64-msvc": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-win32-arm64-msvc/-/rspack-resolver-binding-win32-arm64-msvc-1.2.2.tgz", + "integrity": "sha512-7sWRJumhpXSi2lccX8aQpfFXHsSVASdWndLv8AmD8nDRA/5PBi8IplQVZNx2mYRx6+Bp91Z00kuVqpXO9NfCTg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" + "node_modules/@unrs/rspack-resolver-binding-win32-x64-msvc": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@unrs/rspack-resolver-binding-win32-x64-msvc/-/rspack-resolver-binding-win32-x64-msvc-1.2.2.tgz", + "integrity": "sha512-hewo/UMGP1a7O6FG/ThcPzSJdm/WwrYDNkdGgWl6M18H6K6MSitklomWpT9MUtT5KGj++QJb06va/14QBC4pvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "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" + "node_modules/@vitest/eslint-plugin": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.38.tgz", + "integrity": "sha512-KcOTZyVz8RiM5HyriiDVrP1CyBGuhRxle+lBsmSs6NTJEO/8dKVAq+f5vQzHj1/Kc7bYXSDO6yBe62Zx0t5iaw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/utils": "^8.24.0", + "eslint": ">= 8.57.0", + "typescript": ">= 5.0.0", + "vitest": "*" }, - "engines": { - "node": ">= 0.6" + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vitest": { + "optional": true + } } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -865,6 +1333,8 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -873,6 +1343,7 @@ "version": "8.4.0", "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz", "integrity": "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==", + "license": "MIT", "dependencies": { "acorn": "^8.11.0" }, @@ -884,6 +1355,8 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -895,18 +1368,12 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "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==", + "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -920,15 +1387,19 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -937,15 +1408,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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/array-includes": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -961,10 +1429,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.findlastindex": { + "node_modules/array.prototype.findlast": { "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -981,14 +1451,16 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -998,14 +1470,16 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -1014,19 +1488,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -1035,10 +1527,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" }, @@ -1052,82 +1556,76 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "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/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/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", "dependencies": { - "streamsearch": "^1.1.0" + "fill-range": "^7.1.1" }, "engines": { - "node": ">=10.16.0" + "node": ">=8" } }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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==", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", "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" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -1140,6 +1638,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1148,6 +1648,8 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1162,12 +1664,15 @@ "node_modules/close-with-grace": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/close-with-grace/-/close-with-grace-1.3.0.tgz", - "integrity": "sha512-lvm0rmLIR5bNz4CRKW6YvCfn9Wg5Wb9A8PJ3Bb+hjyikgC1RO1W3J4z9rBXQYw97mAte7dNSQI8BmUsxdlXQyw==" + "integrity": "sha512-lvm0rmLIR5bNz4CRKW6YvCfn9Wg5Wb9A8PJ3Bb+hjyikgC1RO1W3J4z9rBXQYw97mAte7dNSQI8BmUsxdlXQyw==", + "license": "MIT" }, "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==", + "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1178,117 +1683,23 @@ "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/compression/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/compression/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/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" - }, - "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==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" }, "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==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1299,13 +1710,15 @@ } }, "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -1315,27 +1728,31 @@ } }, "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" }, @@ -1347,11 +1764,13 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1365,12 +1784,16 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" }, "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==", + "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -1387,6 +1810,8 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -1399,27 +1824,12 @@ "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/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -1427,70 +1837,79 @@ "node": ">=6.0.0" } }, - "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/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" } }, "node_modules/es-abstract": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.2.tgz", - "integrity": "sha512-60s3Xv2T2p1ICykc7c+DNDPLDMm9t4QxCOUU0K9JxiLjM3C1zB9YVdN7tjxrFd4+AkZ8CdX1ovUga4P2+1e+/w==", + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.5", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" }, "engines": { "node": ">= 0.4" @@ -1500,12 +1919,11 @@ } }, "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" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -1514,14 +1932,46 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, "engines": { "node": ">= 0.4" } }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -1530,34 +1980,44 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -1566,15 +2026,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1583,63 +2040,72 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.23.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", + "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", + "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.2", + "@eslint/config-helpers": "^0.2.0", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.23.0", + "@eslint/plugin-kit": "^0.2.7", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", @@ -1650,76 +2116,177 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, - "node_modules/eslint-module-utils": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", - "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "node_modules/eslint-plugin-import-x": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.9.1.tgz", + "integrity": "sha512-YJ9W12tfDBBYVUUI5FVls6ZrzbVmfrHcQkjeHrG6I7QxWAlIbueRD+G4zPTg1FwlBouunTYm9dhJMVJZdj9wwQ==", + "dev": true, + "license": "MIT", "dependencies": { - "debug": "^3.2.7" + "@types/doctrine": "^0.0.9", + "@typescript-eslint/utils": "^8.27.0", + "debug": "^4.4.0", + "doctrine": "^3.0.0", + "eslint-import-resolver-node": "^0.3.9", + "get-tsconfig": "^4.10.0", + "is-glob": "^4.0.3", + "minimatch": "^10.0.1", + "rspack-resolver": "^1.2.2", + "semver": "^7.7.1", + "stable-hash": "^0.0.5", + "tslib": "^2.8.1" }, "engines": { - "node": ">=4" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-import-x/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/eslint-plugin-import-x/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-jest-dom": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest-dom/-/eslint-plugin-jest-dom-5.5.0.tgz", + "integrity": "sha512-CRlXfchTr7EgC3tDI7MGHY6QjdJU5Vv2RPaeeGtkXUHnKZf04kgzMPIJUXt4qKCvYWVVIEo9ut9Oq1vgXAykEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.16.3", + "requireindex": "^1.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0", + "npm": ">=6", + "yarn": ">=1" + }, + "peerDependencies": { + "@testing-library/dom": "^8.0.0 || ^9.0.0 || ^10.0.0", + "eslint": "^6.8.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" }, "peerDependenciesMeta": { - "eslint": { + "@testing-library/dom": { "optional": true } } }, - "node_modules/eslint-module-utils/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "node_modules/eslint-plugin-playwright": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.2.0.tgz", + "integrity": "sha512-qSQpAw7RcSzE3zPp8FMGkthaCWovHZ/BsXtpmnGax9vQLIovlh1bsZHEa2+j2lv9DWhnyeLM/qZmp7ffQZfQvg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "examples" + ], "dependencies": { - "ms": "^2.1.1" + "globals": "^13.23.0" + }, + "engines": { + "node": ">=16.6.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" } }, - "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "node_modules/eslint-plugin-playwright/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", - "debug": "^3.2.7", + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", + "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", - "is-glob": "^4.0.3", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "tsconfig-paths": "^3.15.0" + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, - "node_modules/eslint-plugin-import/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dependencies": { - "ms": "^2.1.1" + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, - "node_modules/eslint-plugin-import/node_modules/doctrine": { + "node_modules/eslint-plugin-react/node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -1727,52 +2294,106 @@ "node": ">=0.10.0" } }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-testing-library": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-7.1.1.tgz", + "integrity": "sha512-nszC833aZPwB6tik1nMkbFqmtgIXTT0sfJEYs0zMBKMlkQ4to2079yUV96SvmLh00ovSBJI4pgcBC1TiIP8mXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "^8.15.0", + "@typescript-eslint/utils": "^8.15.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0", + "pnpm": "^9.14.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -1784,6 +2405,8 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -1791,274 +2414,255 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exercises__sep__01.init__sep__01.problem.static": { + "resolved": "exercises/01.init/01.problem.static", + "link": true + }, + "node_modules/exercises__sep__01.init__sep__01.solution.static": { + "resolved": "exercises/01.init/01.solution.static", + "link": true + }, + "node_modules/exercises__sep__02.server-components__sep__01.problem.rsc": { + "resolved": "exercises/02.server-components/01.problem.rsc", + "link": true + }, + "node_modules/exercises__sep__02.server-components__sep__01.solution.rsc": { + "resolved": "exercises/02.server-components/01.solution.rsc", + "link": true + }, + "node_modules/exercises__sep__02.server-components__sep__02.problem.async-components": { + "resolved": "exercises/02.server-components/02.problem.async-components", + "link": true + }, + "node_modules/exercises__sep__02.server-components__sep__02.solution.async-components": { + "resolved": "exercises/02.server-components/02.solution.async-components", + "link": true + }, + "node_modules/exercises__sep__02.server-components__sep__03.problem.streaming": { + "resolved": "exercises/02.server-components/03.problem.streaming", + "link": true + }, + "node_modules/exercises__sep__02.server-components__sep__03.solution.streaming": { + "resolved": "exercises/02.server-components/03.solution.streaming", + "link": true + }, + "node_modules/exercises__sep__02.server-components__sep__04.problem.server-context": { + "resolved": "exercises/02.server-components/04.problem.server-context", + "link": true }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } + "node_modules/exercises__sep__02.server-components__sep__04.solution.server-context": { + "resolved": "exercises/02.server-components/04.solution.server-context", + "link": true }, - "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/exercises__sep__03.client-components__sep__01.problem.loader": { + "resolved": "exercises/03.client-components/01.problem.loader", + "link": true }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } + "node_modules/exercises__sep__03.client-components__sep__01.solution.loader": { + "resolved": "exercises/03.client-components/01.solution.loader", + "link": true }, - "node_modules/exercises__sep__01.exercises__sep__01.problem.ssr": { - "resolved": "exercises/01.exercises/01.problem.ssr", + "node_modules/exercises__sep__03.client-components__sep__02.problem.module-resolution": { + "resolved": "exercises/03.client-components/02.problem.module-resolution", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__01.solution.ssr": { - "resolved": "exercises/01.exercises/01.solution.ssr", + "node_modules/exercises__sep__03.client-components__sep__02.solution.module-resolution": { + "resolved": "exercises/03.client-components/02.solution.module-resolution", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__02.problem.server-context": { - "resolved": "exercises/01.exercises/02.problem.server-context", + "node_modules/exercises__sep__04.router__sep__01.problem.router": { + "resolved": "exercises/04.router/01.problem.router", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__02.solution.server-context": { - "resolved": "exercises/01.exercises/02.solution.server-context", + "node_modules/exercises__sep__04.router__sep__01.solution.router": { + "resolved": "exercises/04.router/01.solution.router", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__03.problem.url": { - "resolved": "exercises/01.exercises/03.problem.url", + "node_modules/exercises__sep__04.router__sep__02.problem.pending-ui": { + "resolved": "exercises/04.router/02.problem.pending-ui", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__03.solution.url": { - "resolved": "exercises/01.exercises/03.solution.url", + "node_modules/exercises__sep__04.router__sep__02.solution.pending-ui": { + "resolved": "exercises/04.router/02.solution.pending-ui", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__04.problem.async-components": { - "resolved": "exercises/01.exercises/04.problem.async-components", + "node_modules/exercises__sep__04.router__sep__03.problem.race-conditions": { + "resolved": "exercises/04.router/03.problem.race-conditions", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__04.solution.async-components": { - "resolved": "exercises/01.exercises/04.solution.async-components", + "node_modules/exercises__sep__04.router__sep__03.solution.race-conditions": { + "resolved": "exercises/04.router/03.solution.race-conditions", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__05.problem.bootstrap": { - "resolved": "exercises/01.exercises/05.problem.bootstrap", + "node_modules/exercises__sep__04.router__sep__04.problem.history": { + "resolved": "exercises/04.router/04.problem.history", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__05.solution.bootstrap": { - "resolved": "exercises/01.exercises/05.solution.bootstrap", + "node_modules/exercises__sep__04.router__sep__04.solution.history": { + "resolved": "exercises/04.router/04.solution.history", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__06.problem.import-map": { - "resolved": "exercises/01.exercises/06.problem.import-map", + "node_modules/exercises__sep__04.router__sep__05.problem.cache": { + "resolved": "exercises/04.router/05.problem.cache", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__06.solution.import-map": { - "resolved": "exercises/01.exercises/06.solution.import-map", + "node_modules/exercises__sep__04.router__sep__05.solution.cache": { + "resolved": "exercises/04.router/05.solution.cache", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__07.problem.module-graph": { - "resolved": "exercises/01.exercises/07.problem.module-graph", + "node_modules/exercises__sep__05.actions__sep__01.problem.action-reference": { + "resolved": "exercises/05.actions/01.problem.action-reference", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__07.solution.module-graph": { - "resolved": "exercises/01.exercises/07.solution.module-graph", + "node_modules/exercises__sep__05.actions__sep__01.solution.action-reference": { + "resolved": "exercises/05.actions/01.solution.action-reference", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__08.problem.hydrate": { - "resolved": "exercises/01.exercises/08.problem.hydrate", + "node_modules/exercises__sep__05.actions__sep__02.problem.client": { + "resolved": "exercises/05.actions/02.problem.client", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__08.solution.hydrate": { - "resolved": "exercises/01.exercises/08.solution.hydrate", + "node_modules/exercises__sep__05.actions__sep__02.solution.client": { + "resolved": "exercises/05.actions/02.solution.client", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__09.problem.routing": { - "resolved": "exercises/01.exercises/09.problem.routing", + "node_modules/exercises__sep__05.actions__sep__03.problem.server": { + "resolved": "exercises/05.actions/03.problem.server", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__09.solution.routing": { - "resolved": "exercises/01.exercises/09.solution.routing", + "node_modules/exercises__sep__05.actions__sep__03.solution.server": { + "resolved": "exercises/05.actions/03.solution.server", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__10.problem.actions": { - "resolved": "exercises/01.exercises/10.problem.actions", + "node_modules/exercises__sep__05.actions__sep__04.problem.revalidation": { + "resolved": "exercises/05.actions/04.problem.revalidation", "link": true }, - "node_modules/exercises__sep__01.exercises__sep__10.solution.actions": { - "resolved": "exercises/01.exercises/10.solution.actions", + "node_modules/exercises__sep__05.actions__sep__04.solution.revalidation": { + "resolved": "exercises/05.actions/04.solution.revalidation", "link": true }, - "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", - "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/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/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "node_modules/exercises__sep__05.actions__sep__05.problem.history-revalidation": { + "resolved": "exercises/05.actions/05.problem.history-revalidation", + "link": true }, - "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/exercises__sep__05.actions__sep__05.solution.history-revalidation": { + "resolved": "exercises/05.actions/05.solution.history-revalidation", + "link": true }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", "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" + "to-regex-range": "^5.0.1" }, "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/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": ">=8" } }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -2071,83 +2675,79 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "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/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "is-callable": "^1.2.7" }, "engines": { - "node": ">=14.14" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } }, "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==", + "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -2160,20 +2760,29 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "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==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2182,37 +2791,30 @@ "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/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, - "engines": { - "node": ">=16" + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.4" } }, "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -2221,29 +2823,25 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "license": "MIT", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" + "resolve-pkg-maps": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -2252,25 +2850,27 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dependencies": { - "type-fest": "^0.20.2" - }, + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", "dependencies": { - "define-properties": "^1.1.3" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -2280,31 +2880,34 @@ } }, "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" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -2313,6 +2916,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2321,6 +2926,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -2329,9 +2936,14 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2340,9 +2952,11 @@ } }, "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==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2354,6 +2968,8 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -2368,6 +2984,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -2375,53 +2993,31 @@ "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/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, - "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" - }, + "node_modules/hono": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.7.5.tgz", + "integrity": "sha512-fDOK5W2C1vZACsgLONigdZTRZxuBqFtcKh7bUQ5cVSbwI2RWjloJDcgFOVzbQrlI6pCmhlTsVYZ7zpLj4m4qMQ==", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=16.9.0" } }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2437,52 +3033,57 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "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/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" } }, - "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==", + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, "engines": { - "node": ">= 0.10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2492,23 +3093,30 @@ } }, "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2521,6 +3129,8 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2529,21 +3139,30 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" }, "engines": { @@ -2554,11 +3173,14 @@ } }, "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2571,14 +3193,53 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -2586,10 +3247,12 @@ "node": ">=0.10.0" } }, - "node_modules/is-negative-zero": { + "node_modules/is-map": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2597,12 +3260,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2611,21 +3287,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "engines": { - "node": ">=8" - } - }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -2634,13 +3306,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", - "dependencies": { - "call-bind": "^1.0.7" - }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2648,24 +3319,31 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -2675,11 +3353,15 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2689,12 +3371,27 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -2703,11 +3400,33 @@ } }, "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2716,17 +3435,48 @@ "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2737,45 +3487,46 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" - }, - "node_modules/json5": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", - "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" }, - "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", "dev": true, + "license": "MIT", "dependencies": { - "universalify": "^2.0.0" + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "engines": { + "node": ">=4.0" } }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -2784,6 +3535,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -2796,6 +3549,8 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -2809,81 +3564,63 @@ "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" - }, - "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/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "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" - } + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" }, - "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" + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" }, - "engines": { - "node": ">=4" + "bin": { + "loose-envify": "cli.js" } }, - "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==", + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.4" } }, - "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" - }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 8" } }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=8.6" } }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2891,63 +3628,39 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" - }, - "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" - } + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, + "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 0.4" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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" } @@ -2956,18 +3669,24 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -2977,43 +3696,32 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.groupby": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", - "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2" + "es-object-atoms": "^1.1.1" }, "engines": { "node": ">= 0.4" } }, - "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" }, "engines": { @@ -3023,68 +3731,67 @@ "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/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, + "license": "MIT", "dependencies": { - "mimic-fn": "^4.0.0" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -3099,6 +3806,8 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -3113,6 +3822,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -3120,34 +3831,22 @@ "node": ">=6" } }, - "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-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "engines": { - "node": ">=0.10.0" - } - }, "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==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3155,17 +3854,59 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz", + "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.51.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } }, - "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/playwright-core": { + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz", + "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } }, "node_modules/possible-typed-array-names": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", - "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -3174,15 +3915,18 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -3193,44 +3937,112 @@ "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" - }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.11.tgz", + "integrity": "sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" } }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "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/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -3244,53 +4056,35 @@ "type": "consulting", "url": "https://feross.org/support" } - ] - }, - "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" - } + ], + "license": "MIT" }, "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-gXp1gsHJOVnV5cYMTDxTDOw5VuRIuDq1HmCNZJZNAVUaBB1FWjfBaXZIFG8E84kjfkA87xrLl+9qSdzrFUUHZA==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", "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-H1rmytKh+RaPwFT67WoN43muY++tRe9G3QkVtgTMJ+AjlHWu4lpbAmDfluPqbcpMR5AvlFCzVqElOvhS3y6sZQ==", + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "license": "MIT", "dependencies": { - "scheduler": "0.0.0-experimental-2b036d3f1-20240327" + "scheduler": "^0.25.0" }, "peerDependencies": { - "react": "0.0.0-experimental-2b036d3f1-20240327" + "react": "^19.0.0" } }, "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==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz", + "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" }, @@ -3298,11 +4092,19 @@ "react": ">=16.13.1" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/react-server-dom-esm": { "name": "@kentcdodds/tmp-react-server-dom-esm", - "version": "0.0.0-experimental-2b036d3f1-20240327", - "resolved": "https://registry.npmjs.org/@kentcdodds/tmp-react-server-dom-esm/-/tmp-react-server-dom-esm-0.0.0-experimental-2b036d3f1-20240327.tgz", - "integrity": "sha512-rKqJW2yBtNH9Qtbu8TXQWAwSSSXUPApe3BpM/1QwONKjVin8dQsejfs8qD8FmiYTzIU2aJUH5gdr/NfP32UlMg==", + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/@kentcdodds/tmp-react-server-dom-esm/-/tmp-react-server-dom-esm-19.0.1.tgz", + "integrity": "sha512-yg59PoC5BuF71Kx8kTH0qMt30dKG/uz2Gkn5U6OTKw+S3yVnY50Djf4z772cbLSZxAP/tdIh6r5rUWRxlVdRoQ==", + "license": "MIT", "dependencies": { "acorn-loose": "^8.3.0" }, @@ -3310,24 +4112,52 @@ "node": ">=0.10.0" }, "peerDependencies": { - "react": "0.0.0-experimental-2b036d3f1-20240327", - "react-dom": "0.0.0-experimental-2b036d3f1-20240327" + "react": "^19.0.0", + "react-dom": "^19.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "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==" + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" }, "node_modules/regexp.prototype.flags": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", - "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", - "set-function-name": "^2.0.1" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -3336,18 +4166,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.5" + } + }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3356,37 +4201,61 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, + "node_modules/rspack-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/rspack-resolver/-/rspack-resolver-1.2.2.tgz", + "integrity": "sha512-Fwc19jMBA3g+fxDJH2B4WxwZjE0VaaOL7OX/A4Wn5Zv7bOD/vyPZhzXfaO73Xc2GAlfi96g5fGUa378WbIGfFw==", + "dev": true, + "license": "MIT", "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/JounQin" + }, + "optionalDependencies": { + "@unrs/rspack-resolver-binding-darwin-arm64": "1.2.2", + "@unrs/rspack-resolver-binding-darwin-x64": "1.2.2", + "@unrs/rspack-resolver-binding-freebsd-x64": "1.2.2", + "@unrs/rspack-resolver-binding-linux-arm-gnueabihf": "1.2.2", + "@unrs/rspack-resolver-binding-linux-arm64-gnu": "1.2.2", + "@unrs/rspack-resolver-binding-linux-arm64-musl": "1.2.2", + "@unrs/rspack-resolver-binding-linux-x64-gnu": "1.2.2", + "@unrs/rspack-resolver-binding-linux-x64-musl": "1.2.2", + "@unrs/rspack-resolver-binding-wasm32-wasi": "1.2.2", + "@unrs/rspack-resolver-binding-win32-arm64-msvc": "1.2.2", + "@unrs/rspack-resolver-binding-win32-x64-msvc": "1.2.2" } }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -3401,18 +4270,22 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -3422,19 +4295,15 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "isarray": "^2.0.5" }, "engines": { "node": ">= 0.4" @@ -3443,83 +4312,49 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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-/rt/j4357yyLvsiceZKo2VKb/ltJJDfdW0ip6Z8VyChG39/qH2kayG1ecFp5Rk4LxtUtoioNG24jJhGXSIvZQA==" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", "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" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/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": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "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/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", + "license": "MIT" }, - "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" + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": ">= 0.8.0" + "node": ">=10" } }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -3536,6 +4371,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -3546,15 +4383,27 @@ "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/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } }, "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==", + "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -3566,19 +4415,24 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3587,43 +4441,122 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, "engines": { - "node": ">=14" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, "engines": { - "node": ">= 0.8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, "engines": { - "node": ">=10.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -3633,14 +4566,20 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3649,6 +4588,8 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", @@ -3661,41 +4602,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -3707,6 +4619,8 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -3718,6 +4632,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3725,34 +4641,45 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" - }, - "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==", + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, "engines": { - "node": ">=0.6" + "node": ">=8.0" } }, - "node_modules/tsconfig-paths": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", - "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", - "dependencies": { - "@types/json5": "^0.0.29", - "json5": "^1.0.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -3764,6 +4691,8 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -3771,41 +4700,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -3815,16 +4736,19 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { "node": ">= 0.4" @@ -3834,16 +4758,18 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" }, "engines": { "node": ">= 0.4" @@ -3852,71 +4778,64 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "node_modules/typescript-eslint": { + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.28.0.tgz", + "integrity": "sha512-jfZtxJoHm59bvoCMYCe2BM0/baMswRhMmYhy+w6VfcyHrjxZ0OJe0tGasydCpIpA+A/WIJhTyZfb3EtwNC/kHQ==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "@typescript-eslint/eslint-plugin": "8.28.0", + "@typescript-eslint/parser": "8.28.0", + "@typescript-eslint/utils": "8.28.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, "engines": { - "node": ">= 10.0.0" - } - }, - "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": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, - "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==", + "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -3928,29 +4847,85 @@ } }, "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -3960,15 +4935,22 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index 48d97f9..7b9b0c3 100644 --- a/package.json +++ b/package.json @@ -1,46 +1,56 @@ { "name": "react-server-components", "private": true, - "kcd-workshop": { + "epicshop": { "title": "React Server Components ๐Ÿคน", - "githubRoot": "https://github.com/epicweb-dev/react-server-components/blob/main" + "githubRoot": "https://github.com/epicweb-dev/react-server-components/blob/main", + "subtitle": "Understand React Server Components and Server Functions by building a framework with them.", + "product": { + "host": "www.epicreact.dev", + "slug": "react-server-components", + "displayName": "EpicReact.dev", + "displayNameShort": "EpicReact", + "logo": "/logo.svg", + "discordChannelId": "1285244676286189569", + "discordTags": [ + "1285246046498328627", + "1285245965271302246" + ] + }, + "onboardingVideo": "https://www.epicweb.dev/tips/get-started-with-the-epic-workshop-app-for-react", + "instructor": { + "name": "Kent C. Dodds", + "avatar": "/images/instructor.png", + "๐•": "kentcdodds" + } }, "type": "module", "scripts": { - "postinstall": "cd ./kcdshop && npm install", - "start": "npx --prefix ./kcdshop kcdshop start", - "dev": "npx --prefix ./kcdshop kcdshop start", - "test": "npm run test --silent --prefix playground", - "test:e2e": "npm run test:e2e --silent --prefix playground", - "test:e2e:dev": "npm run test:e2e:dev --silent --prefix playground", - "test:e2e:run": "npm run test:e2e:run --silent --prefix playground", - "setup": "node ./setup.js", - "setup:custom": "node ./kcdshop/setup-custom.js", + "postinstall": "cd ./epicshop && npm install", + "start": "npx --prefix ./epicshop epicshop start", + "dev": "npx --prefix ./epicshop epicshop start", + "test": "node ./epicshop/test.js", + "setup": "node ./epicshop/setup.js", + "setup:custom": "node ./epicshop/setup-custom.js", "format": "prettier --write .", "lint": "eslint ." }, "keywords": [], "author": "Kent C. Dodds (https://kentcdodds.com/)", "license": "GPL-3.0-only", - "eslintIgnore": [ - "/node_modules" - ], "workspaces": [ "exercises/*/*", "examples/*" ], "engines": { - "node": ">=18", + "node": ">=20", "npm": ">=8.16.0", "git": ">=2.18.0" }, "devDependencies": { - "execa": "^8.0.1", - "fs-extra": "^11.1.1", - "prettier": "^3.2.5" + "@epic-web/config": "^1.18.2", + "eslint": "^9.23.0", + "prettier": "^3.5.3" }, - "dependencies": { - "eslint": "^8.57.0", - "eslint-plugin-import": "^2.29.1" - } + "prettier": "@epic-web/config/prettier" } diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..212e0ff Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..4b249f0 --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,13 @@ + + + + diff --git a/public/images/instructor.png b/public/images/instructor.png new file mode 100644 index 0000000..707102e Binary files /dev/null and b/public/images/instructor.png differ diff --git a/public/images/spa-initial-render.png b/public/images/spa-initial-render.png new file mode 100644 index 0000000..b2c234f Binary files /dev/null and b/public/images/spa-initial-render.png differ diff --git a/public/images/spa.png b/public/images/spa.png new file mode 100644 index 0000000..18dfda6 Binary files /dev/null and b/public/images/spa.png differ diff --git a/public/images/super-simple-rsc-initial-render.png b/public/images/super-simple-rsc-initial-render.png new file mode 100644 index 0000000..723fc48 Binary files /dev/null and b/public/images/super-simple-rsc-initial-render.png differ diff --git a/public/images/super-simple-rsc.png b/public/images/super-simple-rsc.png new file mode 100644 index 0000000..3a1888a Binary files /dev/null and b/public/images/super-simple-rsc.png differ diff --git a/public/logo.svg b/public/logo.svg new file mode 100644 index 0000000..1697496 --- /dev/null +++ b/public/logo.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/og/background.png b/public/og/background.png new file mode 100644 index 0000000..1b935a4 Binary files /dev/null and b/public/og/background.png differ diff --git a/public/og/logo.svg b/public/og/logo.svg new file mode 100644 index 0000000..ba85094 --- /dev/null +++ b/public/og/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/setup.js b/setup.js deleted file mode 100644 index 517ecc4..0000000 --- a/setup.js +++ /dev/null @@ -1 +0,0 @@ -import './kcdshop/setup.js'