diff --git a/exercises/01.exercises/04.problem.async-components/src/app.js b/exercises/01.exercises/04.problem.async-components/src/app.js index 3d441e3..6cd23da 100644 --- a/exercises/01.exercises/04.problem.async-components/src/app.js +++ b/exercises/01.exercises/04.problem.async-components/src/app.js @@ -3,7 +3,7 @@ import { shipDataStorage } from '../server/async-storage.js' import { ShipDetails } from './ship-details.js' import { SearchResults } from './ship-search-results.js' -export function Document() { +export async function Document() { return h( 'html', { lang: 'en' }, diff --git a/exercises/01.exercises/04.solution.async-components/src/app.js b/exercises/01.exercises/04.solution.async-components/src/app.js index 9f3b567..90cbc73 100644 --- a/exercises/01.exercises/04.solution.async-components/src/app.js +++ b/exercises/01.exercises/04.solution.async-components/src/app.js @@ -3,7 +3,7 @@ import { shipDataStorage } from '../server/async-storage.js' import { ShipDetails, ShipFallback } from './ship-details.js' import { SearchResults, SearchResultsFallback } from './ship-search-results.js' -export function Document() { +export async function Document() { return h( 'html', { lang: 'en' }, diff --git a/exercises/01.exercises/05.problem.bootstrap/src/app.js b/exercises/01.exercises/05.problem.bootstrap/src/app.js index 9f3b567..90cbc73 100644 --- a/exercises/01.exercises/05.problem.bootstrap/src/app.js +++ b/exercises/01.exercises/05.problem.bootstrap/src/app.js @@ -3,7 +3,7 @@ import { shipDataStorage } from '../server/async-storage.js' import { ShipDetails, ShipFallback } from './ship-details.js' import { SearchResults, SearchResultsFallback } from './ship-search-results.js' -export function Document() { +export async function Document() { return h( 'html', { lang: 'en' }, diff --git a/exercises/01.exercises/05.solution.bootstrap/src/app.js b/exercises/01.exercises/05.solution.bootstrap/src/app.js index 9f3b567..90cbc73 100644 --- a/exercises/01.exercises/05.solution.bootstrap/src/app.js +++ b/exercises/01.exercises/05.solution.bootstrap/src/app.js @@ -3,7 +3,7 @@ import { shipDataStorage } from '../server/async-storage.js' import { ShipDetails, ShipFallback } from './ship-details.js' import { SearchResults, SearchResultsFallback } from './ship-search-results.js' -export function Document() { +export async function Document() { return h( 'html', { lang: 'en' }, diff --git a/exercises/01.exercises/06.problem.import-map/src/app.js b/exercises/01.exercises/06.problem.import-map/src/app.js index 9f3b567..90cbc73 100644 --- a/exercises/01.exercises/06.problem.import-map/src/app.js +++ b/exercises/01.exercises/06.problem.import-map/src/app.js @@ -3,7 +3,7 @@ import { shipDataStorage } from '../server/async-storage.js' import { ShipDetails, ShipFallback } from './ship-details.js' import { SearchResults, SearchResultsFallback } from './ship-search-results.js' -export function Document() { +export async function Document() { return h( 'html', { lang: 'en' }, diff --git a/exercises/01.exercises/06.solution.import-map/server/ssr.js b/exercises/01.exercises/06.solution.import-map/server/ssr.js index 29800ec..04ca6ed 100644 --- a/exercises/01.exercises/06.solution.import-map/server/ssr.js +++ b/exercises/01.exercises/06.solution.import-map/server/ssr.js @@ -18,6 +18,9 @@ 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') diff --git a/exercises/01.exercises/06.solution.import-map/src/app.js b/exercises/01.exercises/06.solution.import-map/src/app.js index 9f3b567..90cbc73 100644 --- a/exercises/01.exercises/06.solution.import-map/src/app.js +++ b/exercises/01.exercises/06.solution.import-map/src/app.js @@ -3,7 +3,7 @@ import { shipDataStorage } from '../server/async-storage.js' import { ShipDetails, ShipFallback } from './ship-details.js' import { SearchResults, SearchResultsFallback } from './ship-search-results.js' -export function Document() { +export async function Document() { return h( 'html', { lang: 'en' }, diff --git a/exercises/01.exercises/10.problem.module-graph/.gitignore b/exercises/01.exercises/07.solution.module-graph/.gitignore similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/.gitignore rename to exercises/01.exercises/07.solution.module-graph/.gitignore diff --git a/exercises/01.exercises/10.problem.module-graph/.prettierignore b/exercises/01.exercises/07.solution.module-graph/.prettierignore similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/.prettierignore rename to exercises/01.exercises/07.solution.module-graph/.prettierignore diff --git a/exercises/01.exercises/10.problem.module-graph/.prettierrc b/exercises/01.exercises/07.solution.module-graph/.prettierrc similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/.prettierrc rename to exercises/01.exercises/07.solution.module-graph/.prettierrc diff --git a/exercises/01.exercises/07.solution.module-graph/README.mdx b/exercises/01.exercises/07.solution.module-graph/README.mdx new file mode 100644 index 0000000..c3bd872 --- /dev/null +++ b/exercises/01.exercises/07.solution.module-graph/README.mdx @@ -0,0 +1 @@ +# Hydrate diff --git a/exercises/01.exercises/10.problem.module-graph/db/ship-api.js b/exercises/01.exercises/07.solution.module-graph/db/ship-api.js similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/db/ship-api.js rename to exercises/01.exercises/07.solution.module-graph/db/ship-api.js diff --git a/exercises/01.exercises/10.problem.module-graph/db/ships.json b/exercises/01.exercises/07.solution.module-graph/db/ships.json similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/db/ships.json rename to exercises/01.exercises/07.solution.module-graph/db/ships.json diff --git a/exercises/01.exercises/10.solution.module-graph/dev.js b/exercises/01.exercises/07.solution.module-graph/dev.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/dev.js rename to exercises/01.exercises/07.solution.module-graph/dev.js diff --git a/exercises/01.exercises/10.problem.module-graph/package-lock.json b/exercises/01.exercises/07.solution.module-graph/package-lock.json similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/package-lock.json rename to exercises/01.exercises/07.solution.module-graph/package-lock.json diff --git a/exercises/01.exercises/10.solution.module-graph/package.json b/exercises/01.exercises/07.solution.module-graph/package.json similarity index 93% rename from exercises/01.exercises/10.solution.module-graph/package.json rename to exercises/01.exercises/07.solution.module-graph/package.json index 19b0e4b..1d95125 100644 --- a/exercises/01.exercises/10.solution.module-graph/package.json +++ b/exercises/01.exercises/07.solution.module-graph/package.json @@ -1,5 +1,5 @@ { - "name": "exercises__sep__01.exercises__sep__10.solution.module-graph", + "name": "exercises__sep__01.exercises__sep__07.solution.module-graph", "version": "1.0.0", "type": "module", "private": true, diff --git a/exercises/01.exercises/10.problem.module-graph/public/favicon.ico b/exercises/01.exercises/07.solution.module-graph/public/favicon.ico similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/favicon.ico rename to exercises/01.exercises/07.solution.module-graph/public/favicon.ico diff --git a/exercises/01.exercises/10.problem.module-graph/public/favicon.svg b/exercises/01.exercises/07.solution.module-graph/public/favicon.svg similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/favicon.svg rename to exercises/01.exercises/07.solution.module-graph/public/favicon.svg diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/broken-ship.webp b/exercises/01.exercises/07.solution.module-graph/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/broken-ship.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/broken-ship.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/fallback-ship.png b/exercises/01.exercises/07.solution.module-graph/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/fallback-ship.png rename to exercises/01.exercises/07.solution.module-graph/public/img/fallback-ship.png diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/0268fc4817ad1.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/0268fc4817ad1.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/1ae7b4b92036b.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/1ae7b4b92036b.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/1ff1991efe029.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/1ff1991efe029.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/3ba8aa65ffe6c.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/441f7092a8d44.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/441f7092a8d44.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/5c13d8b28a14a.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/5c13d8b28a14a.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/627c497212456.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/627c497212456.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/670003aed3795.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/670003aed3795.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/6c86fca8b9086.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/6c86fca8b9086.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/6f375578ead88.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/6f375578ead88.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/ab267a5984523.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/ab267a5984523.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/b442531ea32b2.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/b442531ea32b2.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/bc4cbadf89bd3.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/bc4cbadf89bd3.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/cb03cc4e5717e.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/cb03cc4e5717e.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/cfd10fcd2de6c.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/cfd10fcd2de6c.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/d3b8aa65ffe6c.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/d486d48b82b81.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/d486d48b82b81.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/e92cefe4f6727.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/e92cefe4f6727.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/ec7a3f950f99f.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/ec7a3f950f99f.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/f3d9a88e1c234.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/f3d9a88e1c234.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/img/ships/fdc13cb488bf1.webp b/exercises/01.exercises/07.solution.module-graph/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/img/ships/fdc13cb488bf1.webp rename to exercises/01.exercises/07.solution.module-graph/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/01.exercises/10.problem.module-graph/public/style.css b/exercises/01.exercises/07.solution.module-graph/public/style.css similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/public/style.css rename to exercises/01.exercises/07.solution.module-graph/public/style.css diff --git a/exercises/01.exercises/10.problem.module-graph/server/async-storage.js b/exercises/01.exercises/07.solution.module-graph/server/async-storage.js similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/server/async-storage.js rename to exercises/01.exercises/07.solution.module-graph/server/async-storage.js diff --git a/exercises/01.exercises/10.solution.module-graph/server/register-rsc-loader.js b/exercises/01.exercises/07.solution.module-graph/server/register-rsc-loader.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/server/register-rsc-loader.js rename to exercises/01.exercises/07.solution.module-graph/server/register-rsc-loader.js diff --git a/exercises/01.exercises/10.solution.module-graph/server/rsc-loader.js b/exercises/01.exercises/07.solution.module-graph/server/rsc-loader.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/server/rsc-loader.js rename to exercises/01.exercises/07.solution.module-graph/server/rsc-loader.js diff --git a/exercises/01.exercises/10.problem.module-graph/server/rsc.js b/exercises/01.exercises/07.solution.module-graph/server/rsc.js similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/server/rsc.js rename to exercises/01.exercises/07.solution.module-graph/server/rsc.js diff --git a/exercises/01.exercises/10.solution.module-graph/server/ssr.js b/exercises/01.exercises/07.solution.module-graph/server/ssr.js similarity index 96% rename from exercises/01.exercises/10.solution.module-graph/server/ssr.js rename to exercises/01.exercises/07.solution.module-graph/server/ssr.js index 00d36cb..99b5aa2 100644 --- a/exercises/01.exercises/10.solution.module-graph/server/ssr.js +++ b/exercises/01.exercises/07.solution.module-graph/server/ssr.js @@ -35,6 +35,9 @@ 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') diff --git a/exercises/01.exercises/10.solution.module-graph/src/app.js b/exercises/01.exercises/07.solution.module-graph/src/app.js similarity index 98% rename from exercises/01.exercises/10.solution.module-graph/src/app.js rename to exercises/01.exercises/07.solution.module-graph/src/app.js index f247cb8..0fae21a 100644 --- a/exercises/01.exercises/10.solution.module-graph/src/app.js +++ b/exercises/01.exercises/07.solution.module-graph/src/app.js @@ -7,7 +7,7 @@ import { ShipDetails, ShipFallback, ShipError } from './ship-details.js' import { SearchResults, SearchResultsFallback } from './ship-search-results.js' import { ShipSearch } from './ship-search.js' -export function Document() { +export async function Document() { return h( 'html', { lang: 'en' }, diff --git a/exercises/01.exercises/10.solution.module-graph/src/error-boundary.js b/exercises/01.exercises/07.solution.module-graph/src/error-boundary.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/src/error-boundary.js rename to exercises/01.exercises/07.solution.module-graph/src/error-boundary.js diff --git a/exercises/01.exercises/10.problem.module-graph/src/img-utils.js b/exercises/01.exercises/07.solution.module-graph/src/img-utils.js similarity index 100% rename from exercises/01.exercises/10.problem.module-graph/src/img-utils.js rename to exercises/01.exercises/07.solution.module-graph/src/img-utils.js diff --git a/exercises/01.exercises/10.solution.module-graph/src/img.js b/exercises/01.exercises/07.solution.module-graph/src/img.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/src/img.js rename to exercises/01.exercises/07.solution.module-graph/src/img.js diff --git a/exercises/01.exercises/10.solution.module-graph/src/index.js b/exercises/01.exercises/07.solution.module-graph/src/index.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/src/index.js rename to exercises/01.exercises/07.solution.module-graph/src/index.js diff --git a/exercises/01.exercises/10.solution.module-graph/src/router.js b/exercises/01.exercises/07.solution.module-graph/src/router.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/src/router.js rename to exercises/01.exercises/07.solution.module-graph/src/router.js diff --git a/exercises/01.exercises/10.solution.module-graph/src/ship-details-pending.js b/exercises/01.exercises/07.solution.module-graph/src/ship-details-pending.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/src/ship-details-pending.js rename to exercises/01.exercises/07.solution.module-graph/src/ship-details-pending.js diff --git a/exercises/01.exercises/10.solution.module-graph/src/ship-details.js b/exercises/01.exercises/07.solution.module-graph/src/ship-details.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/src/ship-details.js rename to exercises/01.exercises/07.solution.module-graph/src/ship-details.js diff --git a/exercises/01.exercises/10.solution.module-graph/src/ship-search-results.js b/exercises/01.exercises/07.solution.module-graph/src/ship-search-results.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/src/ship-search-results.js rename to exercises/01.exercises/07.solution.module-graph/src/ship-search-results.js diff --git a/exercises/01.exercises/10.solution.module-graph/src/ship-search.js b/exercises/01.exercises/07.solution.module-graph/src/ship-search.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/src/ship-search.js rename to exercises/01.exercises/07.solution.module-graph/src/ship-search.js diff --git a/exercises/01.exercises/10.solution.module-graph/src/spin-delay.js b/exercises/01.exercises/07.solution.module-graph/src/spin-delay.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/src/spin-delay.js rename to exercises/01.exercises/07.solution.module-graph/src/spin-delay.js diff --git a/exercises/01.exercises/10.solution.module-graph/.gitignore b/exercises/01.exercises/08.solution.hydrate/.gitignore similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/.gitignore rename to exercises/01.exercises/08.solution.hydrate/.gitignore diff --git a/exercises/01.exercises/10.solution.module-graph/.prettierignore b/exercises/01.exercises/08.solution.hydrate/.prettierignore similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/.prettierignore rename to exercises/01.exercises/08.solution.hydrate/.prettierignore diff --git a/exercises/01.exercises/10.solution.module-graph/.prettierrc b/exercises/01.exercises/08.solution.hydrate/.prettierrc similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/.prettierrc rename to exercises/01.exercises/08.solution.hydrate/.prettierrc diff --git a/exercises/01.exercises/08.solution.hydrate/README.mdx b/exercises/01.exercises/08.solution.hydrate/README.mdx new file mode 100644 index 0000000..c3bd872 --- /dev/null +++ b/exercises/01.exercises/08.solution.hydrate/README.mdx @@ -0,0 +1 @@ +# Hydrate diff --git a/exercises/01.exercises/10.solution.module-graph/db/ship-api.js b/exercises/01.exercises/08.solution.hydrate/db/ship-api.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/db/ship-api.js rename to exercises/01.exercises/08.solution.hydrate/db/ship-api.js diff --git a/exercises/01.exercises/10.solution.module-graph/db/ships.json b/exercises/01.exercises/08.solution.hydrate/db/ships.json similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/db/ships.json rename to exercises/01.exercises/08.solution.hydrate/db/ships.json diff --git a/exercises/01.exercises/10.problem.module-graph/dev.js b/exercises/01.exercises/08.solution.hydrate/dev.js similarity index 91% rename from exercises/01.exercises/10.problem.module-graph/dev.js rename to exercises/01.exercises/08.solution.hydrate/dev.js index 81780de..b10826e 100644 --- a/exercises/01.exercises/10.problem.module-graph/dev.js +++ b/exercises/01.exercises/08.solution.hydrate/dev.js @@ -35,7 +35,13 @@ const ssrServer = spawnScript( const rscServer = spawnScript( 'node', - ['--watch', '--conditions=react-server', 'server/rsc.js'], + [ + '--watch', + '--import', + './server/register-rsc-loader.js', + '--conditions=react-server', + 'server/rsc.js', + ], { PORT: RSC_PORT }, chalk.green.bgBlack('RSC'), ) diff --git a/exercises/01.exercises/10.solution.module-graph/package-lock.json b/exercises/01.exercises/08.solution.hydrate/package-lock.json similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/package-lock.json rename to exercises/01.exercises/08.solution.hydrate/package-lock.json diff --git a/exercises/01.exercises/10.problem.module-graph/package.json b/exercises/01.exercises/08.solution.hydrate/package.json similarity index 92% rename from exercises/01.exercises/10.problem.module-graph/package.json rename to exercises/01.exercises/08.solution.hydrate/package.json index aa43790..e5625ee 100644 --- a/exercises/01.exercises/10.problem.module-graph/package.json +++ b/exercises/01.exercises/08.solution.hydrate/package.json @@ -1,5 +1,5 @@ { - "name": "exercises__sep__01.exercises__sep__10.problem.module-graph", + "name": "exercises__sep__01.exercises__sep__08.solution.hydrate", "version": "1.0.0", "type": "module", "private": true, diff --git a/exercises/01.exercises/10.solution.module-graph/public/favicon.ico b/exercises/01.exercises/08.solution.hydrate/public/favicon.ico similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/favicon.ico rename to exercises/01.exercises/08.solution.hydrate/public/favicon.ico diff --git a/exercises/01.exercises/10.solution.module-graph/public/favicon.svg b/exercises/01.exercises/08.solution.hydrate/public/favicon.svg similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/favicon.svg rename to exercises/01.exercises/08.solution.hydrate/public/favicon.svg diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/broken-ship.webp b/exercises/01.exercises/08.solution.hydrate/public/img/broken-ship.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/broken-ship.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/broken-ship.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/fallback-ship.png b/exercises/01.exercises/08.solution.hydrate/public/img/fallback-ship.png similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/fallback-ship.png rename to exercises/01.exercises/08.solution.hydrate/public/img/fallback-ship.png diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/0268fc4817ad1.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/0268fc4817ad1.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/0268fc4817ad1.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/0268fc4817ad1.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/1ae7b4b92036b.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/1ae7b4b92036b.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/1ae7b4b92036b.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/1ae7b4b92036b.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/1ff1991efe029.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/1ff1991efe029.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/1ff1991efe029.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/1ff1991efe029.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/3ba8aa65ffe6c.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/3ba8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/3ba8aa65ffe6c.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/3ba8aa65ffe6c.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/441f7092a8d44.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/441f7092a8d44.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/441f7092a8d44.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/441f7092a8d44.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/5c13d8b28a14a.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/5c13d8b28a14a.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/5c13d8b28a14a.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/5c13d8b28a14a.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/627c497212456.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/627c497212456.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/627c497212456.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/627c497212456.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/670003aed3795.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/670003aed3795.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/670003aed3795.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/670003aed3795.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/6c86fca8b9086.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/6c86fca8b9086.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/6c86fca8b9086.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/6c86fca8b9086.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/6f375578ead88.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/6f375578ead88.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/6f375578ead88.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/6f375578ead88.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/ab267a5984523.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/ab267a5984523.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/ab267a5984523.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/ab267a5984523.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/b442531ea32b2.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/b442531ea32b2.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/b442531ea32b2.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/b442531ea32b2.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/bc4cbadf89bd3.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/bc4cbadf89bd3.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/bc4cbadf89bd3.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/bc4cbadf89bd3.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/cb03cc4e5717e.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/cb03cc4e5717e.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/cb03cc4e5717e.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/cb03cc4e5717e.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/cfd10fcd2de6c.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/cfd10fcd2de6c.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/cfd10fcd2de6c.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/cfd10fcd2de6c.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/d3b8aa65ffe6c.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/d3b8aa65ffe6c.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/d3b8aa65ffe6c.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/d3b8aa65ffe6c.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/d486d48b82b81.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/d486d48b82b81.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/d486d48b82b81.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/d486d48b82b81.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/e92cefe4f6727.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/e92cefe4f6727.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/e92cefe4f6727.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/e92cefe4f6727.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/ec7a3f950f99f.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/ec7a3f950f99f.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/ec7a3f950f99f.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/ec7a3f950f99f.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/f3d9a88e1c234.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/f3d9a88e1c234.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/f3d9a88e1c234.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/f3d9a88e1c234.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/img/ships/fdc13cb488bf1.webp b/exercises/01.exercises/08.solution.hydrate/public/img/ships/fdc13cb488bf1.webp similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/img/ships/fdc13cb488bf1.webp rename to exercises/01.exercises/08.solution.hydrate/public/img/ships/fdc13cb488bf1.webp diff --git a/exercises/01.exercises/10.solution.module-graph/public/style.css b/exercises/01.exercises/08.solution.hydrate/public/style.css similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/public/style.css rename to exercises/01.exercises/08.solution.hydrate/public/style.css diff --git a/exercises/01.exercises/10.solution.module-graph/server/async-storage.js b/exercises/01.exercises/08.solution.hydrate/server/async-storage.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/server/async-storage.js rename to exercises/01.exercises/08.solution.hydrate/server/async-storage.js diff --git a/exercises/01.exercises/08.solution.hydrate/server/register-rsc-loader.js b/exercises/01.exercises/08.solution.hydrate/server/register-rsc-loader.js new file mode 100644 index 0000000..ba86d1a --- /dev/null +++ b/exercises/01.exercises/08.solution.hydrate/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/01.exercises/08.solution.hydrate/server/rsc-loader.js b/exercises/01.exercises/08.solution.hydrate/server/rsc-loader.js new file mode 100644 index 0000000..836ca6f --- /dev/null +++ b/exercises/01.exercises/08.solution.hydrate/server/rsc-loader.js @@ -0,0 +1,23 @@ +import { resolve, load as reactLoad } from 'react-server-dom-esm/node-loader' + +export { resolve } + +async function textLoad(url, context, defaultLoad) { + const result = await defaultLoad(url, context, defaultLoad) + if (result.format === 'module') { + if (typeof result.source === 'string') { + return result + } + return { + source: Buffer.from(result.source).toString('utf8'), + format: 'module', + } + } + return result +} + +export async function load(url, context, defaultLoad) { + return await reactLoad(url, context, (u, c) => { + return textLoad(u, c, defaultLoad) + }) +} diff --git a/exercises/01.exercises/10.solution.module-graph/server/rsc.js b/exercises/01.exercises/08.solution.hydrate/server/rsc.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/server/rsc.js rename to exercises/01.exercises/08.solution.hydrate/server/rsc.js diff --git a/exercises/01.exercises/08.solution.hydrate/server/ssr.js b/exercises/01.exercises/08.solution.hydrate/server/ssr.js new file mode 100644 index 0000000..99b5aa2 --- /dev/null +++ b/exercises/01.exercises/08.solution.hydrate/server/ssr.js @@ -0,0 +1,172 @@ +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) { + // Proxy the request to the rsc server. + const proxiedHeaders = { + 'X-Forwarded-Host': req.hostname, + 'X-Forwarded-For': req.ips, + 'X-Forwarded-Port': PORT, + 'X-Forwarded-Proto': req.protocol, + } + if (req.get('Content-Type')) { + proxiedHeaders['Content-Type'] = req.get('Content-Type') + } + + const promiseForData = request( + { + host: RSC_ORIGIN.hostname, + port: RSC_ORIGIN.port, + method: req.method, + path: req.url, + headers: proxiedHeaders, + }, + req, + ) + + if (req.accepts('text/html')) { + try { + const rscResponse = await promiseForData + const moduleBaseURL = '/js/src' + + // For HTML, we're a "client" emulator that runs the client code, + // so we start by consuming the RSC payload. This needs the local file path + // to load the source files from as well as the URL path for preloads. + + let contentPromise + function Root() { + contentPromise ??= createFromNodeStream( + rscResponse, + moduleBasePath, + moduleBaseURL, + ) + const content = use(contentPromise) + return content.root + } + const location = req.url + const navigate = () => { + throw new Error('navigate cannot be called on the server') + } + const isPending = false + const routerValue = { + location, + nextLocation: location, + navigate, + isPending, + } + const { pipe } = renderToPipeableStream( + h(RouterContext.Provider, { value: routerValue }, h(Root)), + { + bootstrapModules: ['/js/src/index.js'], + importMap: { + imports: { + react: + 'https://esm.sh/react@0.0.0-experimental-2b036d3f1-20240327?pin=v126&dev', + 'react-dom': + 'https://esm.sh/react-dom@0.0.0-experimental-2b036d3f1-20240327?pin=v126&dev', + 'react-dom/': + 'https://esm.sh/react-dom@0.0.0-experimental-2b036d3f1-20240327&pin=v126&dev/', + 'react-error-boundary': + 'https://esm.sh/react-error-boundary@4.0.13?pin=126&dev', + 'react-server-dom-esm/client': '/js/react-server-dom-esm/client', + }, + }, + }, + ) + pipe(res) + } catch (e) { + console.error(`Failed to SSR: ${e.stack}`) + res.statusCode = 500 + res.end(`Failed to SSR: ${e.stack}`) + } + } else { + try { + const rscResponse = await promiseForData + + // Forward all headers from the RSC response to the client response + Object.entries(rscResponse.headers).forEach(([header, value]) => { + res.set(header, value) + }) + + if (req.get('rsc-action')) { + res.set('Content-type', 'text/x-component') + } + + rscResponse.on('data', data => { + res.write(data) + res.flush() + }) + rscResponse.on('end', () => { + res.end() + }) + } catch (e) { + console.error(`Failed to proxy request: ${e.stack}`) + res.statusCode = 500 + res.end(`Failed to proxy request: ${e.stack}`) + } + } +}) + +const server = app.listen(PORT, () => { + console.log(`✅ SSR: http://localhost:${PORT}`) +}) + +closeWithGrace(async ({ signal, err }) => { + if (err) console.error('Shutting down server due to error', err) + else console.log('Shutting down server due to signal', signal) + + await new Promise((resolve, reject) => { + server.close(err => { + if (err) reject(err) + else resolve() + }) + }) +}) diff --git a/exercises/01.exercises/08.solution.hydrate/src/app.js b/exercises/01.exercises/08.solution.hydrate/src/app.js new file mode 100644 index 0000000..0fae21a --- /dev/null +++ b/exercises/01.exercises/08.solution.hydrate/src/app.js @@ -0,0 +1,79 @@ +import { createElement as h, Suspense } from 'react' +import { shipDataStorage } from '../server/async-storage.js' +import { ErrorBoundary } from './error-boundary.js' +import { shipFallbackSrc } from './img-utils.js' +import { ShipDetailsPendingTransition } from './ship-details-pending.js' +import { ShipDetails, ShipFallback, ShipError } from './ship-details.js' +import { SearchResults, SearchResultsFallback } from './ship-search-results.js' +import { ShipSearch } from './ship-search.js' + +export async function Document() { + return h( + 'html', + { lang: 'en' }, + h( + 'head', + null, + h('meta', { charSet: 'utf-8' }), + h('meta', { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }), + h('title', null, 'Super Simple RSC'), + h('link', { rel: 'stylesheet', href: '/style.css' }), + h('link', { + rel: 'shortcut icon', + type: 'image/svg+xml', + href: '/favicon.svg', + }), + ), + h('body', null, h('div', { className: 'app-wrapper' }, h(App))), + ) +} + +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/08.solution.hydrate/src/error-boundary.js b/exercises/01.exercises/08.solution.hydrate/src/error-boundary.js new file mode 100644 index 0000000..9a93e62 --- /dev/null +++ b/exercises/01.exercises/08.solution.hydrate/src/error-boundary.js @@ -0,0 +1,4 @@ +// https://github.com/bvaughn/react-error-boundary/issues/182 +'use client' + +export { ErrorBoundary } from 'react-error-boundary' diff --git a/exercises/01.exercises/10.solution.module-graph/src/img-utils.js b/exercises/01.exercises/08.solution.hydrate/src/img-utils.js similarity index 100% rename from exercises/01.exercises/10.solution.module-graph/src/img-utils.js rename to exercises/01.exercises/08.solution.hydrate/src/img-utils.js diff --git a/exercises/01.exercises/08.solution.hydrate/src/img.js b/exercises/01.exercises/08.solution.hydrate/src/img.js new file mode 100644 index 0000000..a59c5fb --- /dev/null +++ b/exercises/01.exercises/08.solution.hydrate/src/img.js @@ -0,0 +1,24 @@ +'use client' + +import { use, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { imgSrc } from './img-utils.js' + +const shipFallbackSrc = '/img/fallback-ship.png' + +export function ShipImg(props) { + return h( + ErrorBoundary, + { fallback: h('img', props), key: props.src }, + h( + Suspense, + { fallback: h('img', { ...props, src: shipFallbackSrc }) }, + h(Img, props), + ), + ) +} + +function Img({ src = '', ...props }) { + src = use(imgSrc(src)) + return h('img', { src, ...props }) +} diff --git a/exercises/01.exercises/08.solution.hydrate/src/index.js b/exercises/01.exercises/08.solution.hydrate/src/index.js new file mode 100644 index 0000000..e954f57 --- /dev/null +++ b/exercises/01.exercises/08.solution.hydrate/src/index.js @@ -0,0 +1,101 @@ +'use client' + +import { + createElement as h, + startTransition, + use, + useEffect, + 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' + +const initialLocation = getGlobalLocation() +const initialContentPromise = RSC.createFromFetch( + fetchContent(initialLocation), + { moduleBaseURL }, +) + +export function Root() { + const latestNav = useRef(null) + const [location, setLocation] = useState(getGlobalLocation) + const [nextLocation, setNextLocation] = useState(location) + const [contentPromise, setContentPromise] = useState(initialContentPromise) + const [isPending, startTransition] = useTransition() + + useEffect(() => { + // once the transition has completed, we can update the current location + if (!isPending) setLocation(nextLocation) + }, [isPending]) + + useEffect(() => { + function handlePopState() { + navigate(getGlobalLocation(), { updateHistory: false }) + } + window.addEventListener('popstate', handlePopState) + return () => window.removeEventListener('popstate', handlePopState) + }, []) + + function createFromFetch(fetchPromise) { + return RSC.createFromFetch(fetchPromise, { moduleBaseURL }) + } + + async function navigate( + nextLocation, + { updateHistory = true, replace = false } = {}, + ) { + if (updateHistory) { + setNextLocation(nextLocation) + } + const thisNav = Symbol() + latestNav.current = thisNav + + const nextContentPromise = createFromFetch( + fetchContent(nextLocation).then(response => { + if (thisNav !== latestNav.current) return + const newLocation = response.headers.get('x-location') + if (updateHistory) { + if (replace) { + window.history.replaceState(null, '', newLocation) + } else { + window.history.pushState(null, '', newLocation) + } + } + return response + }), + ) + + startTransition(() => { + setContentPromise(nextContentPromise) + }) + } + + return h( + RouterContext.Provider, + { + value: { + location, + nextLocation: isPending ? nextLocation : location, + navigate, + isPending, + }, + }, + use(contentPromise).root, + ) +} + +startTransition(() => { + hydrateRoot(document, h(Root)) +}) diff --git a/exercises/01.exercises/08.solution.hydrate/src/router.js b/exercises/01.exercises/08.solution.hydrate/src/router.js new file mode 100644 index 0000000..248e678 --- /dev/null +++ b/exercises/01.exercises/08.solution.hydrate/src/router.js @@ -0,0 +1,34 @@ +import { createContext, use } from 'react' + +export const RouterContext = createContext() + +export function useRouter() { + const context = use(RouterContext) + if (!context) { + throw new Error('useRouter must be used within a Router') + } + return context +} + +export function parseLocationState(location) { + const url = new URL(location, 'http://example.com') + return { + shipId: url.pathname.split('/').at(1), + search: url.searchParams.get('search'), + } +} + +export function serializeLocationState({ shipId, search }) { + const pathname = shipId ? `/${shipId}` : '/' + const searchParams = new URLSearchParams() + if (search) { + searchParams.set('search', search) + } + return [pathname, searchParams.toString()].filter(Boolean).join('?') +} + +export function mergeLocationState(location, updates) { + const currentState = parseLocationState(location) + const nextState = { ...currentState, ...updates } + return serializeLocationState(nextState) +} diff --git a/exercises/01.exercises/08.solution.hydrate/src/ship-details-pending.js b/exercises/01.exercises/08.solution.hydrate/src/ship-details-pending.js new file mode 100644 index 0000000..0d98be6 --- /dev/null +++ b/exercises/01.exercises/08.solution.hydrate/src/ship-details-pending.js @@ -0,0 +1,21 @@ +'use client' + +import { createElement as h } from 'react' +import { parseLocationState, useRouter } from './router.js' +import { useSpinDelay } from './spin-delay.js' + +export function ShipDetailsPendingTransition({ children }) { + const { location, nextLocation } = useRouter() + const previousShipId = parseLocationState(nextLocation).shipId + const nextShipId = parseLocationState(location).shipId + const isShipDetailsPending = useSpinDelay(previousShipId !== nextShipId, { + delay: 300, + minDuration: 350, + }) + + return h('div', { + className: 'details', + style: { opacity: isShipDetailsPending ? 0.6 : 1 }, + children, + }) +} diff --git a/exercises/01.exercises/10.problem.module-graph/src/ship-details.js b/exercises/01.exercises/08.solution.hydrate/src/ship-details.js similarity index 95% rename from exercises/01.exercises/10.problem.module-graph/src/ship-details.js rename to exercises/01.exercises/08.solution.hydrate/src/ship-details.js index 995bf3c..1e12822 100644 --- a/exercises/01.exercises/10.problem.module-graph/src/ship-details.js +++ b/exercises/01.exercises/08.solution.hydrate/src/ship-details.js @@ -2,6 +2,7 @@ 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() @@ -13,7 +14,7 @@ export async function ShipDetails() { h( 'div', { className: 'ship-info__img-wrapper' }, - h('img', { src: shipImgSrc, alt: ship.name }), + 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')), @@ -54,7 +55,7 @@ export function ShipFallback() { h( 'div', { className: 'ship-info__img-wrapper' }, - h('img', { + h(ShipImg, { src: getImageUrlForShip(shipId, { size: 200 }), // TODO: handle this better alt: shipId, diff --git a/exercises/01.exercises/10.problem.module-graph/src/ship-search-results.js b/exercises/01.exercises/08.solution.hydrate/src/ship-search-results.js similarity index 71% rename from exercises/01.exercises/10.problem.module-graph/src/ship-search-results.js rename to exercises/01.exercises/08.solution.hydrate/src/ship-search-results.js index b686499..fb4f956 100644 --- a/exercises/01.exercises/10.problem.module-graph/src/ship-search-results.js +++ b/exercises/01.exercises/08.solution.hydrate/src/ship-search-results.js @@ -2,34 +2,27 @@ 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 => { - const href = [ - `/${ship.id}`, - search ? `search=${encodeURIComponent(search)}` : null, - ] - .filter(Boolean) - .join('?') - return h( + return shipResults.ships.map(ship => + h( 'li', { key: ship.name }, h( - 'a', - { - href, - style: { fontWeight: ship.id === currentShipId ? 'bold' : 'normal' }, - }, - h('img', { + SelectShipLink, + { shipId: ship.id, highlight: ship.id === currentShipId }, + h(ShipImg, { src: getImageUrlForShip(ship.id, { size: 20 }), alt: ship.name, }), ship.name, ), - ) - }) + ), + ) } export function SearchResultsFallback() { diff --git a/exercises/01.exercises/08.solution.hydrate/src/ship-search.js b/exercises/01.exercises/08.solution.hydrate/src/ship-search.js new file mode 100644 index 0000000..b7f6899 --- /dev/null +++ b/exercises/01.exercises/08.solution.hydrate/src/ship-search.js @@ -0,0 +1,68 @@ +'use client' + +import { Fragment, Suspense, createElement as h } from 'react' +import { ErrorBoundary } from './error-boundary.js' +import { parseLocationState, mergeLocationState, useRouter } from './router.js' +import { useSpinDelay } from './spin-delay.js' + +export function ShipSearch({ search, results, fallback }) { + const { navigate, location, nextLocation } = useRouter() + const previousSearch = parseLocationState(nextLocation).search + const nextSearch = parseLocationState(location).search + const isShipSearchPending = useSpinDelay(previousSearch !== nextSearch, { + delay: 300, + minDuration: 350, + }) + + return h( + Fragment, + null, + h( + 'form', + { onSubmit: e => e.preventDefault() }, + h('input', { + placeholder: 'Filter ships...', + type: 'search', + defaultValue: search, + name: 'search', + autoFocus: true, + onChange: event => { + const newLocation = mergeLocationState(location, { + search: event.currentTarget.value, + }) + navigate(newLocation, { replace: true }) + }, + }), + ), + h( + ErrorBoundary, + { + fallback: h( + 'div', + { style: { padding: 6, color: '#CD0DD5' } }, + 'There was an error retrieving results', + ), + }, + h( + 'ul', + { style: { opacity: isShipSearchPending ? 0.6 : 1 } }, + h(Suspense, { fallback }, results), + ), + ), + ) +} + +export function 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/08.solution.hydrate/src/spin-delay.js b/exercises/01.exercises/08.solution.hydrate/src/spin-delay.js new file mode 100644 index 0000000..e5fd221 --- /dev/null +++ b/exercises/01.exercises/08.solution.hydrate/src/spin-delay.js @@ -0,0 +1,35 @@ +import { useState, useEffect, useRef } from 'react' + +export const defaultOptions = { + delay: 500, + minDuration: 200, +} + +export function useSpinDelay(loading, options) { + options = Object.assign({}, defaultOptions, options) + const [state, setState] = useState('IDLE') + const timeout = useRef(null) + useEffect(() => { + if (loading && state === 'IDLE') { + clearTimeout(timeout.current) + timeout.current = setTimeout(() => { + if (!loading) { + return setState('IDLE') + } + timeout.current = setTimeout(() => { + setState('EXPIRE') + }, options.minDuration) + setState('DISPLAY') + }, options.delay) + setState('DELAY') + } + if (!loading && state !== 'DISPLAY') { + clearTimeout(timeout.current) + setState('IDLE') + } + }, [loading, state, options.delay, options.minDuration]) + useEffect(() => { + return () => clearTimeout(timeout.current) + }, []) + return state === 'DISPLAY' || state === 'EXPIRE' +} diff --git a/exercises/01.exercises/10.problem.module-graph/README.mdx b/exercises/01.exercises/10.problem.module-graph/README.mdx deleted file mode 100644 index 9b4c162..0000000 --- a/exercises/01.exercises/10.problem.module-graph/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# Module Graph diff --git a/exercises/01.exercises/10.problem.module-graph/server/ssr.js b/exercises/01.exercises/10.problem.module-graph/server/ssr.js deleted file mode 100644 index 793ac76..0000000 --- a/exercises/01.exercises/10.problem.module-graph/server/ssr.js +++ /dev/null @@ -1,92 +0,0 @@ -import http from 'node:http' -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 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.all('/:shipId?', async function (req, res) { - // Proxy the request to the rsc server. - const proxiedHeaders = { - 'X-Forwarded-Host': req.hostname, - 'X-Forwarded-For': req.ips, - 'X-Forwarded-Port': PORT, - 'X-Forwarded-Proto': req.protocol, - } - if (req.get('Content-Type')) { - proxiedHeaders['Content-Type'] = req.get('Content-Type') - } - - const promiseForData = request( - { - host: RSC_ORIGIN.hostname, - port: RSC_ORIGIN.port, - method: req.method, - path: req.url, - headers: proxiedHeaders, - }, - req, - ) - - try { - const rscResponse = await promiseForData - - // For HTML, we're a "client" emulator that runs the client code, - // so we start by consuming the RSC payload. This needs the local file path - // to load the source files from as well as the URL path for preloads. - - let contentPromise - function Root() { - contentPromise ??= createFromNodeStream(rscResponse) - const content = use(contentPromise) - return content.root - } - const { pipe } = renderToPipeableStream(h(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/10.problem.module-graph/src/app.js b/exercises/01.exercises/10.problem.module-graph/src/app.js deleted file mode 100644 index 9f3b567..0000000 --- a/exercises/01.exercises/10.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 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/10.solution.module-graph/README.mdx b/exercises/01.exercises/10.solution.module-graph/README.mdx deleted file mode 100644 index 9b4c162..0000000 --- a/exercises/01.exercises/10.solution.module-graph/README.mdx +++ /dev/null @@ -1 +0,0 @@ -# Module Graph diff --git a/exercises/01.exercises/99.solution.final/server/ssr.js b/exercises/01.exercises/99.solution.final/server/ssr.js index b0109af..78db696 100644 --- a/exercises/01.exercises/99.solution.final/server/ssr.js +++ b/exercises/01.exercises/99.solution.final/server/ssr.js @@ -35,6 +35,9 @@ 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') diff --git a/exercises/01.exercises/99.solution.final/src/app.js b/exercises/01.exercises/99.solution.final/src/app.js index f247cb8..0fae21a 100644 --- a/exercises/01.exercises/99.solution.final/src/app.js +++ b/exercises/01.exercises/99.solution.final/src/app.js @@ -7,7 +7,7 @@ import { ShipDetails, ShipFallback, ShipError } from './ship-details.js' import { SearchResults, SearchResultsFallback } from './ship-search-results.js' import { ShipSearch } from './ship-search.js' -export function Document() { +export async function Document() { return h( 'html', { lang: 'en' },