odoro 1.0.8 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +94 -0
  2. package/client.d.ts +90 -16
  3. package/dist/build-LZAUDJST.js +5 -0
  4. package/dist/{chunk-34RGOZFA.js → chunk-4D56Z7G3.js} +49 -50
  5. package/dist/{chunk-JMEHF3KN.js → chunk-7ZB5MAM6.js} +4 -4
  6. package/dist/{chunk-LHZGX5ML.js → chunk-OMHEQVIW.js} +295 -160
  7. package/dist/{chunk-3SZIN6VG.js → chunk-P3DJITWE.js} +10 -10
  8. package/dist/{chunk-ZVL7EXJO.js → chunk-PRWVZ2SM.js} +22 -14
  9. package/dist/chunk-Q2KCMTB5.js +741 -0
  10. package/dist/chunk-QOVBLN7A.js +241 -0
  11. package/dist/chunk-WWTDEV4Y.js +604 -0
  12. package/dist/cli.d.ts +12 -13
  13. package/dist/cli.js +84 -63
  14. package/dist/{commands-4JRBD55Z.js → commands-EUGVLQWL.js} +44 -45
  15. package/dist/{commands-AHMXBWQQ.js → commands-GYA7NTCX.js} +124 -124
  16. package/dist/{create-CDKS7KZ3.js → create-QX5LEWMS.js} +158 -154
  17. package/dist/index.d.ts +603 -151
  18. package/dist/index.js +6 -6
  19. package/dist/{package-NBP5NDX6.js → package-T4Z7OBOH.js} +5 -5
  20. package/dist/preview-P4WY5GEY.js +4 -0
  21. package/dist/registry/index.d.ts +75 -76
  22. package/dist/registry/index.js +1 -1
  23. package/dist/{server-4UGN3SFS.js → server-2WP562EV.js} +3 -3
  24. package/package.json +5 -5
  25. package/templates/react-ts/README.md +71 -35
  26. package/templates/react-ts/_env.example +9 -0
  27. package/templates/react-ts/_variants/with-engine/src/background.tsx +118 -0
  28. package/templates/react-ts/_variants/without-libs/src/App.tsx +380 -0
  29. package/templates/react-ts/_variants/without-libs/src/background.tsx +24 -0
  30. package/templates/react-ts/_variants/without-libs/src/entry-server.tsx +39 -0
  31. package/templates/react-ts/_variants/without-libs/src/main.tsx +26 -0
  32. package/templates/{react-ts-server/_variantes/sans-libs/client → react-ts/_variants/without-libs}/src/styles.css +137 -137
  33. package/templates/react-ts/{_variantes/sans-routeur → _variants/without-router}/src/App.tsx +141 -141
  34. package/templates/react-ts/_variants/without-router/src/entry-server.tsx +39 -0
  35. package/templates/react-ts/index.html +4 -4
  36. package/templates/react-ts/odoro.config.ts +5 -0
  37. package/templates/react-ts/src/App.tsx +219 -209
  38. package/templates/react-ts/src/background.tsx +66 -0
  39. package/templates/react-ts/src/entry-server.tsx +85 -0
  40. package/templates/react-ts/src/main.tsx +13 -4
  41. package/templates/react-ts/src/router.tsx +65 -45
  42. package/templates/react-ts/src/styles.css +5 -5
  43. package/templates/react-ts-server/Dockerfile +9 -9
  44. package/templates/react-ts-server/README.md +97 -45
  45. package/templates/react-ts-server/_env.example +31 -33
  46. package/templates/react-ts-server/_variants/with-engine/client/src/background.tsx +118 -0
  47. package/templates/react-ts-server/_variants/without-libs/client/src/App.tsx +380 -0
  48. package/templates/react-ts-server/_variants/without-libs/client/src/background.tsx +24 -0
  49. package/templates/react-ts-server/_variants/without-libs/client/src/entry-server.tsx +39 -0
  50. package/templates/react-ts-server/_variants/without-libs/client/src/main.tsx +26 -0
  51. package/templates/{react-ts/_variantes/sans-libs → react-ts-server/_variants/without-libs/client}/src/styles.css +137 -137
  52. package/templates/react-ts-server/{_variantes/sans-routeur → _variants/without-router}/client/src/App.tsx +141 -141
  53. package/templates/react-ts-server/_variants/without-router/client/src/entry-server.tsx +39 -0
  54. package/templates/react-ts-server/client/index.html +4 -4
  55. package/templates/react-ts-server/client/src/App.tsx +228 -210
  56. package/templates/react-ts-server/client/src/account.tsx +267 -0
  57. package/templates/react-ts-server/client/src/auth.tsx +139 -0
  58. package/templates/react-ts-server/client/src/background.tsx +66 -0
  59. package/templates/react-ts-server/client/src/entry-server.tsx +92 -0
  60. package/templates/react-ts-server/client/src/main.tsx +17 -5
  61. package/templates/react-ts-server/client/src/router.tsx +77 -45
  62. package/templates/react-ts-server/client/src/styles.css +5 -5
  63. package/templates/react-ts-server/odoro.config.ts +7 -4
  64. package/templates/react-ts-server/package.json +2 -0
  65. package/templates/react-ts-server/scripts/dev.mjs +14 -14
  66. package/templates/react-ts-server/server/src/main.ts +119 -45
  67. package/templates/react-ts-server/server/src/modules/auth/index.ts +238 -0
  68. package/templates/react-ts-server/server/src/modules/auth/password.ts +122 -0
  69. package/templates/react-ts-server/server/src/modules/auth/store.ts +193 -0
  70. package/templates/react-ts-server/server/src/modules/health/index.ts +45 -46
  71. package/dist/build-JFQHODAT.js +0 -5
  72. package/dist/chunk-22KJTV2R.js +0 -380
  73. package/dist/chunk-DL3NPC4H.js +0 -113
  74. package/dist/chunk-TQUJ3MFS.js +0 -268
  75. package/dist/preview-7DKEAQOQ.js +0 -4
  76. package/templates/react-ts/_variantes/avec-moteur/src/fond.tsx +0 -118
  77. package/templates/react-ts/_variantes/sans-libs/src/App.tsx +0 -382
  78. package/templates/react-ts/_variantes/sans-libs/src/fond.tsx +0 -23
  79. package/templates/react-ts/_variantes/sans-libs/src/main.tsx +0 -17
  80. package/templates/react-ts/src/fond.tsx +0 -42
  81. package/templates/react-ts-server/_variantes/avec-moteur/client/src/fond.tsx +0 -118
  82. package/templates/react-ts-server/_variantes/sans-libs/client/src/App.tsx +0 -382
  83. package/templates/react-ts-server/_variantes/sans-libs/client/src/fond.tsx +0 -23
  84. package/templates/react-ts-server/_variantes/sans-libs/client/src/main.tsx +0 -17
  85. package/templates/react-ts-server/client/src/fond.tsx +0 -42
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The decorative background of the page.
3
+ *
4
+ * ## Why it is not in `App.tsx`
5
+ *
6
+ * It is the only piece of the page that depends on what was ticked at
7
+ * creation: with the engine, this file is replaced by a version that opens an
8
+ * animated WebGL surface; without it, it is the gradient below.
9
+ *
10
+ * Keeping it apart avoids having two `App.tsx` files to maintain — one per
11
+ * case — which would diverge at the first change of wording. `App.tsx` places
12
+ * `<Background />` and asks for nothing more.
13
+ *
14
+ * ## Why the sizes are inline styles
15
+ *
16
+ * The style system **emits no arbitrary-value class**: `o-h-[42rem]` produces
17
+ * no rule at all, and a missing class paints nothing. The container ended up
18
+ * zero pixels tall, so did both glows, and the background was nowhere to be
19
+ * seen — with nothing to report it.
20
+ *
21
+ * The utilities cover the values on the scale; anything off the scale is
22
+ * written as a style, where it is safe.
23
+ *
24
+ * @module
25
+ */
26
+
27
+ import type { ReactElement } from 'react'
28
+
29
+ /** Two brand glows, heavily diluted, behind the top of the page. */
30
+ export function Background(): ReactElement {
31
+ return (
32
+ <div
33
+ aria-hidden="true"
34
+ className="o-pointer-events-none o-absolute o-inset-x-0 o-top-0 o-overflow-hidden"
35
+ style={{ height: '42rem' }}
36
+ >
37
+ <div
38
+ className="o-absolute o-rounded-full"
39
+ style={{
40
+ left: '50%',
41
+ top: '-18rem',
42
+ width: '46rem',
43
+ height: '46rem',
44
+ transform: 'translateX(-50%)',
45
+ filter: 'blur(64px)',
46
+ opacity: 0.3,
47
+ background:
48
+ 'radial-gradient(closest-side, var(--o-palette-brand-500), transparent)',
49
+ }}
50
+ />
51
+ <div
52
+ className="o-absolute o-rounded-full"
53
+ style={{
54
+ right: '-10rem',
55
+ top: '6rem',
56
+ width: '30rem',
57
+ height: '30rem',
58
+ filter: 'blur(64px)',
59
+ opacity: 0.18,
60
+ background:
61
+ 'radial-gradient(closest-side, var(--o-palette-brand-400), transparent)',
62
+ }}
63
+ />
64
+ </div>
65
+ )
66
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The prerendering entry point.
3
+ *
4
+ * ## What it does, and why it exists
5
+ *
6
+ * `odoro build` renders each of the routes below to HTML, at build time, and
7
+ * writes one complete document per route. The site stays a set of static files
8
+ * — nothing runs in production — but a search engine, a link preview or a
9
+ * screen reader find text in the very first response, instead of an empty
10
+ * container.
11
+ *
12
+ * `src/main.tsx` then takes over: seeing a container already filled, it
13
+ * hydrates instead of rebuilding.
14
+ *
15
+ * ## Adding a route
16
+ *
17
+ * A single line in `routes`. The path becomes a directory in `dist/`:
18
+ * `/pricing` gives `dist/pricing/index.html`, which any host serves with no
19
+ * configuration.
20
+ *
21
+ * ## Doing without
22
+ *
23
+ * Remove `prerender` from `odoro.config.ts`. This file can then go: nothing
24
+ * else imports it.
25
+ *
26
+ * @module
27
+ */
28
+
29
+ import { StrictMode } from 'react'
30
+ import { renderToString } from 'react-dom/server'
31
+
32
+ import { App } from '@/App'
33
+
34
+ /** The routes rendered at build time. */
35
+ export const routes = ['/', '/about']
36
+
37
+ /** What a page brings to the head of the document. */
38
+ const TITLES: Readonly<Record<string, { title: string; description: string }>> = {
39
+ '/': {
40
+ title: 'Odoro',
41
+ description: 'An Odoro project: in-house engine, generated styles, built-in router.',
42
+ },
43
+ '/about': {
44
+ title: 'About — Odoro',
45
+ description: 'What this project ships, and what each package does in it.',
46
+ },
47
+ }
48
+
49
+ /** Escapes what goes into an HTML attribute. */
50
+ function escape(text: string): string {
51
+ return text
52
+ .replace(/&/g, '&amp;')
53
+ .replace(/</g, '&lt;')
54
+ .replace(/>/g, '&gt;')
55
+ .replace(/"/g, '&quot;')
56
+ }
57
+
58
+ /**
59
+ * Renders a route.
60
+ *
61
+ * @param url The requested path.
62
+ * @returns The HTML of the container, and the tags to add to the head.
63
+ */
64
+ export function render(url: string): { html: string; head: string } {
65
+ const meta = TITLES[url] ?? TITLES['/']
66
+
67
+ const head = [
68
+ `<title>${escape(meta?.title ?? 'Odoro')}</title>`,
69
+ `<meta name="description" content="${escape(meta?.description ?? '')}">`,
70
+ `<meta property="og:title" content="${escape(meta?.title ?? 'Odoro')}">`,
71
+ `<meta property="og:description" content="${escape(meta?.description ?? '')}">`,
72
+ ].join('\n ')
73
+
74
+ return {
75
+ // `StrictMode` is present on both sides: a tree rendered without it here
76
+ // and with it in the browser does not always produce the same markup, and
77
+ // hydration then reports a difference that is not one.
78
+ html: renderToString(
79
+ <StrictMode>
80
+ <App url={url} />
81
+ </StrictMode>,
82
+ ),
83
+ head,
84
+ }
85
+ }
@@ -1,5 +1,5 @@
1
1
  import { StrictMode } from 'react'
2
- import { createRoot } from 'react-dom/client'
2
+ import { createRoot, hydrateRoot } from 'react-dom/client'
3
3
 
4
4
  import { App } from '@/App'
5
5
 
@@ -8,11 +8,20 @@ import '@/styles.css'
8
8
 
9
9
  const container = document.getElementById('root')
10
10
  if (container === null) {
11
- throw new Error('Element racine "#root" introuvable dans index.html.')
11
+ throw new Error('Root element "#root" not found in index.html.')
12
12
  }
13
13
 
14
- createRoot(container).render(
14
+ const tree = (
15
15
  <StrictMode>
16
16
  <App />
17
- </StrictMode>,
17
+ </StrictMode>
18
18
  )
19
+
20
+ // A container that is already filled comes from prerendering: it has to be
21
+ // **hydrated**, that is, taken over as it stands with the events attached.
22
+ // Rebuilding it would throw away the page the visitor already sees only to draw
23
+ // it again identically — a flicker, and the whole benefit of prerendering lost.
24
+ //
25
+ // Empty, it is an ordinary render.
26
+ if (container.firstElementChild === null) createRoot(container).render(tree)
27
+ else hydrateRoot(container, tree)
@@ -1,72 +1,92 @@
1
1
  /**
2
- * Le routeur du projet.
2
+ * The router of the project.
3
3
  *
4
- * ## Pourquoi il vit seul, dans son fichier
4
+ * ## Why it lives alone, in its own file
5
5
  *
6
- * C'est le seul endroit qui nomme la dependance de routage. Changer de routeur,
7
- * ajouter une route, en proteger une derriere une authentification : tout se
8
- * lit ici, et `App.tsx` n'a rien a en savoir au-dela de la ligne qui l'importe.
6
+ * This is the only place that names the routing dependency. Switching routers,
7
+ * adding a route, putting one behind authentication: it all reads here, and
8
+ * `App.tsx` needs to know nothing beyond the line that imports it.
9
9
  *
10
- * ## Pourquoi les pages sont passees, et non importees
10
+ * ## Why the pages are passed in, and not imported
11
11
  *
12
- * Ce fichier pourrait importer les pages depuis `App.tsx`. Les deux modules
13
- * s'importeraient alors l'un l'autre : cela fonctionne, mais l'ordre
14
- * d'evaluation devient une question a laquelle personne ne veut repondre le
15
- * jour ou quelque chose s'execute au chargement du module.
12
+ * This file could import the pages from `App.tsx`. The two modules would then
13
+ * import each other: that works, but evaluation order becomes a question
14
+ * nobody wants to answer the day something runs at module load.
16
15
  *
17
- * Les pages arrivent donc en proprietes. La dependance ne va que dans un sens —
18
- * `App.tsx` connait le routeur, le routeur ne connait que React — et la table
19
- * des routes reste lisible d'un coup d'oeil.
16
+ * So the pages arrive as properties. The dependency only goes one way —
17
+ * `App.tsx` knows the router, the router only knows React — and the route
18
+ * table stays readable at a glance.
20
19
  *
21
20
  * @module
22
21
  */
23
22
 
24
- import { Outlet, Route, Router, Routes } from '@odoro-cli/libs/router'
23
+ import {
24
+ Outlet,
25
+ Route,
26
+ // Aliased: the component exported below is the one the application sees, and
27
+ // it takes the name `Router`.
28
+ Router as RouterProvider,
29
+ Routes,
30
+ createMemoryHistory,
31
+ } from '@odoro-cli/libs/router'
25
32
  import type { ReactElement, ReactNode } from 'react'
26
33
 
27
34
  export { Link, useLocation } from '@odoro-cli/libs/router'
28
35
 
29
- /** Les pages que le routeur place. */
30
- export interface RouteurProps {
31
- /** L'enveloppe commune : navigation, contenu, pied de page. */
32
- readonly enveloppe: (contenu: ReactNode) => ReactElement
33
- /** La page d'accueil. */
34
- readonly accueil: ReactElement
35
- /** La page « A propos ». */
36
- readonly apropos: ReactElement
37
- /** Ce qui s'affiche quand aucune route ne correspond. */
38
- readonly introuvable: ReactElement
36
+ /** The pages the router places. */
37
+ export interface RouterProps {
38
+ /** The common shell: navigation, content, footer. */
39
+ readonly shell: (content: ReactNode) => ReactElement
40
+ /** The home page. */
41
+ readonly home: ReactElement
42
+ /** The "About" page. */
43
+ readonly about: ReactElement
44
+ /** What shows when no route matches. */
45
+ readonly notFound: ReactElement
46
+ /**
47
+ * The address to render, when there is no address bar.
48
+ *
49
+ * That is the case while prerendering: the page is made on the build
50
+ * machine, where `window` does not exist and nothing says which route is
51
+ * being asked for. The in-memory history takes over then.
52
+ *
53
+ * In the browser it stays absent and the router reads the real address.
54
+ */
55
+ readonly url?: string
39
56
  }
40
57
 
41
58
  /**
42
- * La table des routes.
59
+ * The route table.
43
60
  *
44
61
  * @example
45
- * <Routeur
46
- * enveloppe={(contenu) => <Coquille>{contenu}</Coquille>}
47
- * accueil={<Accueil />}
48
- * apropos={<APropos />}
49
- * introuvable={<Introuvable />}
62
+ * <Router
63
+ * shell={(content) => <Shell>{content}</Shell>}
64
+ * home={<Home />}
65
+ * about={<About />}
66
+ * notFound={<NotFound />}
50
67
  * />
51
68
  */
52
- export function Routeur({
53
- enveloppe,
54
- accueil,
55
- apropos,
56
- introuvable,
57
- }: RouteurProps): ReactElement {
69
+ export function Router({
70
+ shell,
71
+ home,
72
+ about,
73
+ notFound,
74
+ url,
75
+ }: RouterProps): ReactElement {
58
76
  return (
59
- <Router>
77
+ <RouterProvider
78
+ history={url === undefined ? undefined : createMemoryHistory([url])}
79
+ >
60
80
  <Routes>
61
- {/* `Outlet` marque l'endroit ou la page courante se rend : c'est ce
62
- qui permet a la navigation et au pied de page de ne pas etre
63
- remontes a chaque changement de route. */}
64
- <Route path="/" element={enveloppe(<Outlet />)}>
65
- <Route index element={accueil} />
66
- <Route path="a-propos" element={apropos} />
67
- <Route path="*" element={introuvable} />
81
+ {/* `Outlet` marks the place where the current page renders: that is
82
+ what keeps the navigation and the footer from being remounted on
83
+ every route change. */}
84
+ <Route path="/" element={shell(<Outlet />)}>
85
+ <Route index element={home} />
86
+ <Route path="about" element={about} />
87
+ <Route path="*" element={notFound} />
68
88
  </Route>
69
89
  </Routes>
70
- </Router>
90
+ </RouterProvider>
71
91
  )
72
92
  }
@@ -1,9 +1,9 @@
1
- /* Styles propres a l'application.
1
+ /* Styles that belong to the application.
2
2
 
3
- Tout le reste vient des jetons Odoro : surcharger une variable ci-dessous
4
- retheme l'ensemble, composants de la librairie compris. La teinte de marque
5
- est `--o-palette-brand-500` ; la remplacer suffit a changer la couleur de la
6
- page, du signe et des boutons d'un seul coup. */
3
+ Everything else comes from the Odoro tokens: overriding a variable below
4
+ rethemes the whole thing, library components included. The brand hue is
5
+ `--o-palette-brand-500`; replacing it is enough to change the colour of the
6
+ page, of the mark and of the buttons in one go. */
7
7
 
8
8
  .app-shell {
9
9
  min-height: 100dvh;
@@ -1,8 +1,8 @@
1
1
  # syntax=docker/dockerfile:1
2
2
 
3
- # --- Etape 1 : dependances -----------------------------------------------
4
- # Isolee pour que le cache de couches survive a toute modification du code :
5
- # seule une modification du manifeste relance l'installation.
3
+ # --- Stage 1: dependencies -----------------------------------------------
4
+ # Isolated so that the layer cache survives any change to the code: only a
5
+ # change to the manifest re-runs the install.
6
6
  FROM node:22-alpine AS deps
7
7
  WORKDIR /app
8
8
  COPY package.json package-lock.json* pnpm-lock.yaml* yarn.lock* ./
@@ -14,28 +14,28 @@ RUN if [ -f pnpm-lock.yaml ]; then \
14
14
  npm ci; \
15
15
  fi
16
16
 
17
- # --- Etape 2 : compilation ------------------------------------------------
17
+ # --- Stage 2: build -------------------------------------------------------
18
18
  FROM node:22-alpine AS build
19
19
  WORKDIR /app
20
20
  COPY --from=deps /app/node_modules ./node_modules
21
21
  COPY . .
22
22
  RUN npm run build
23
23
 
24
- # --- Etape 3 : dependances de production ----------------------------------
25
- # Reinstallees a part : l'image finale ne doit pas embarquer la chaine de
26
- # compilation, qui pese souvent plus que l'application elle-meme.
24
+ # --- Stage 3: production dependencies -------------------------------------
25
+ # Reinstalled separately: the final image must not carry the build chain,
26
+ # which often weighs more than the application itself.
27
27
  FROM node:22-alpine AS runtime-deps
28
28
  WORKDIR /app
29
29
  COPY package.json package-lock.json* ./
30
30
  RUN npm ci --omit=dev || npm install --omit=dev
31
31
 
32
- # --- Etape 4 : image finale -----------------------------------------------
32
+ # --- Stage 4: final image -------------------------------------------------
33
33
  FROM node:22-alpine AS runtime
34
34
  WORKDIR /app
35
35
  ENV NODE_ENV=production
36
36
  ENV PORT=3001
37
37
 
38
- # Un processus applicatif n'a aucune raison de tourner en root.
38
+ # An application process has no reason to run as root.
39
39
  RUN addgroup -S odoro && adduser -S odoro -G odoro
40
40
 
41
41
  COPY --from=runtime-deps --chown=odoro:odoro /app/node_modules ./node_modules
@@ -1,16 +1,16 @@
1
- # Application Odoro — client et serveur
1
+ # Odoro application — client and server
2
2
 
3
- Une application monopage React et une API bâtie sur `@odoro-cli/server`, dans le
4
- même dépôt.
3
+ A React single-page application and an API built on `@odoro-cli/server`, in the
4
+ same repository.
5
5
 
6
6
  ```
7
- client/ interface, servie par Odoro en developpement
7
+ client/ interface, served by Odoro in development
8
8
  server/
9
- src/main.ts assemble les modules, rien d'autre
10
- src/modules/ un dossier par module — commencez par `health/`
9
+ src/main.ts assembles the modules, nothing else
10
+ src/modules/ one directory per module — start with `health/`
11
11
  ```
12
12
 
13
- ## Demarrer
13
+ ## Getting started
14
14
 
15
15
  ```sh
16
16
  cp .env.example .env
@@ -18,30 +18,58 @@ npm install
18
18
  npm run dev
19
19
  ```
20
20
 
21
- Le client ecoute sur <http://localhost:5180> et transmet au serveur tout appel
22
- commencant par `/api`. Le navigateur ne voit donc qu'une seule origine, et
23
- aucune question de CORS ne se pose en developpement.
21
+ The client listens on <http://localhost:5180> and forwards every call starting
22
+ with `/api` to the server. The browser therefore only ever sees one origin, and
23
+ no CORS question arises in development.
24
24
 
25
- ## La base de donnees
25
+ ## Authentication, as a demonstration
26
26
 
27
- **PostgreSQL, heberge, et rien d'autre.** Il n'y a pas de base locale : l'URL
28
- pointe sur une base joignable par le reseau.
27
+ The template ships a working account system: register, sign in, sign out,
28
+ profile. Four routes under `/api/auth/`, three pages at `/sign-in`, `/register`
29
+ and `/profile`.
30
+
31
+ What it does, and how:
32
+
33
+ - the password is hashed with **scrypt**, from the Node standard library — no
34
+ native module to compile. The cost parameters travel with the hash, so the
35
+ passwords already stored still verify the day the cost is raised;
36
+ - the session lives in an **`httpOnly`** cookie: the browser sends it, no
37
+ script reads it. The table keeps only its fingerprint, never the identifier;
38
+ - an unknown address and a wrong password give **the same answer**, after the
39
+ same delay. Telling them apart would turn the sign-in form into a way to ask
40
+ whether an address has an account here.
41
+
42
+ What it does not do, and what you will have to add: email confirmation,
43
+ password reset, a rate limit on sign-in attempts, a second factor. Naming them
44
+ beats implying they are handled.
45
+
46
+ It all lives in `server/src/modules/auth/` and `client/src/account.tsx`. If the
47
+ project needs no accounts, delete those two and the `auth.module` line of
48
+ `server/src/main.ts`.
49
+
50
+ **A database is required.** Without `DATABASE_URL`, the four routes answer
51
+ `503` saying what is missing — the interface still starts.
52
+
53
+ ## The database
54
+
55
+ **PostgreSQL, hosted, and nothing else.** There is no local database: the URL
56
+ points at one reachable over the network.
29
57
 
30
58
  ```sh
31
- odoro db:create # provisionne une base et ecrit .env
32
- # ou collez votre propre URL dans .env :
33
- DATABASE_URL=postgres://utilisateur:motdepasse@hote:5432/base?sslmode=require
59
+ odoro db:create # provisions a database and writes .env
60
+ # or paste your own URL into .env:
61
+ DATABASE_URL=postgres://user:password@host:5432/database?sslmode=require
34
62
  ```
35
63
 
36
- Tant qu'elle est absente, **le client demarre quand meme** et le serveur repond
37
- `503` sur `/api/ready` en disant precisement ce qui manque. On voit donc
38
- l'interface des la premiere minute, et on sait ce qu'il reste a faire.
64
+ While it is missing, **the client starts anyway** and the server answers `503`
65
+ on `/api/ready` saying precisely what is absent. So you see the interface in
66
+ the first minute, and you know what is left to do.
39
67
 
40
- ## Ecrire un module
68
+ ## Writing a module
41
69
 
42
- `server/src/modules/health/` est l'exemple. Un module declare son nom, ce dont
43
- il a besoin, les services qu'il enregistre et les routes qu'il expose — et
44
- `main.ts` ne fait que dire lesquels sont actifs.
70
+ `server/src/modules/health/` is the example. A module declares its name, what
71
+ it needs, the services it registers and the routes it exposes — and `main.ts`
72
+ does nothing but say which ones are active.
45
73
 
46
74
  ```ts
47
75
  export const billingModule = defineModule({
@@ -52,36 +80,60 @@ export const billingModule = defineModule({
52
80
  })
53
81
  ```
54
82
 
55
- Le noyau resout l'ordre de chargement depuis `requires`, detecte les cycles, et
56
- refuse de demarrer si une dependance manque. Activer ou desactiver un module
57
- tient donc en une ligne dans `main.ts`.
83
+ The kernel resolves the load order from `requires`, detects cycles, and refuses
84
+ to start if a dependency is missing. Enabling or disabling a module therefore
85
+ takes one line in `main.ts`.
58
86
 
59
- ## Deux points de controle, qui ne disent pas la meme chose
87
+ ## Two health endpoints, which do not say the same thing
60
88
 
61
- | Route | Question | Qui l'interroge |
62
- | ------------- | ------------------------------- | ------------------------------------------------ |
63
- | `/api/health` | Le processus vit-il ? | L'orchestrateur, pour decider de redemarrer |
64
- | `/api/ready` | Le service peut-il travailler ? | Le repartiteur, pour decider d'envoyer du trafic |
89
+ | Route | Question | Who asks it |
90
+ | ------------- | ----------------------------- | --------------------------------------- |
91
+ | `/api/health` | Is the process alive? | The orchestrator, to decide on a restart |
92
+ | `/api/ready` | Can the service do its work? | The load balancer, to decide on traffic |
65
93
 
66
- Les confondre donne l'un des deux defauts : un service qui redemarre en boucle
67
- pendant un incident de base, ou un repartiteur qui envoie du trafic a un
68
- service incapable de repondre.
94
+ Confusing them gives one of two faults: a service restarting in a loop during a
95
+ database incident, or a load balancer sending traffic to a service that cannot
96
+ answer.
69
97
 
70
- ## Compiler et deployer
98
+ ## Building and deploying
71
99
 
72
100
  ```sh
73
- npm run build # client dans dist/client, serveur dans dist/server
74
- npm start # sert les deux depuis un seul processus
101
+ npm run build # client into dist/client, server into dist/server
102
+ npm start # serves both from a single process
75
103
  ```
76
104
 
77
- Le `Dockerfile` est multi-etapes : les dependances de compilation ne se
78
- retrouvent pas dans l'image finale, qui tourne sous un utilisateur sans
79
- privileges.
105
+ The `Dockerfile` is multi-stage: the build dependencies do not end up in the
106
+ final image, which runs as an unprivileged user.
107
+
108
+ ## Prerendering
109
+
110
+ `build.prerender` is on: every route of `client/src/entry-server.tsx` is
111
+ rendered as a complete HTML document at build time, and the client hydrates it
112
+ on load rather than rebuilding everything. That is what lets a search engine or
113
+ a link preview find text in the very first response.
114
+
115
+ The server serves those documents before falling back to the single-page
116
+ document: a request for `/about` receives `dist/client/about/index.html`, with
117
+ its own title and its own description.
118
+
119
+ Adding a route: one line in `routes`, and the matching entry in
120
+ `client/src/router.tsx`. Doing without: remove `prerender` from
121
+ `odoro.config.ts`, then delete `client/src/entry-server.tsx`.
122
+
123
+ ## Environment variables
124
+
125
+ See `.env.example`, commented line by line. In production, whatever is missing
126
+ stops the startup with a message that **lists everything at once** — rather
127
+ than a string of restarts, one variable at a time.
80
128
 
81
- ## Variables d'environnement
129
+ The files are read from the least specific to the most specific — `.env`,
130
+ `.env.local`, `.env.<mode>`, `.env.<mode>.local` — and a variable already set
131
+ in the environment is never overwritten by a file: on a host, whatever it
132
+ injects wins.
82
133
 
83
- Voir `.env.example`, commente ligne par ligne. En production, ce qui manque
84
- arrete le demarrage avec un message qui **liste tout d'un coup** — plutot
85
- qu'une suite de redemarrages, une variable a la fois.
134
+ **Only variables prefixed with `ODORO_` reach the browser.** The rest —
135
+ `DATABASE_URL`, the session secrets, the API keys — never leaves the server.
136
+ That is the only boundary that matters here, and it is held by the prefix, not
137
+ by the file.
86
138
 
87
- `.env` n'est jamais versionne.
139
+ `.env` is never committed.