odoro 1.0.9 → 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-CYVSVDAD.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-EIDLKAA6.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 -66
  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 -66
@@ -1,72 +1,104 @@
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
+ /** The sign-in form. */
45
+ readonly signIn: ReactElement
46
+ /** The registration form. */
47
+ readonly register: ReactElement
48
+ /** The profile of the signed-in account. */
49
+ readonly profile: ReactElement
50
+ /** What shows when no route matches. */
51
+ readonly notFound: ReactElement
52
+ /**
53
+ * The address to render, when there is no address bar.
54
+ *
55
+ * That is the case while prerendering: the page is made on the build
56
+ * machine, where `window` does not exist and nothing says which route is
57
+ * being asked for. The in-memory history takes over then.
58
+ *
59
+ * In the browser it stays absent and the router reads the real address.
60
+ */
61
+ readonly url?: string
39
62
  }
40
63
 
41
64
  /**
42
- * La table des routes.
65
+ * The route table.
43
66
  *
44
67
  * @example
45
- * <Routeur
46
- * enveloppe={(contenu) => <Coquille>{contenu}</Coquille>}
47
- * accueil={<Accueil />}
48
- * apropos={<APropos />}
49
- * introuvable={<Introuvable />}
68
+ * <Router
69
+ * shell={(content) => <Shell>{content}</Shell>}
70
+ * home={<Home />}
71
+ * about={<About />}
72
+ * notFound={<NotFound />}
50
73
  * />
51
74
  */
52
- export function Routeur({
53
- enveloppe,
54
- accueil,
55
- apropos,
56
- introuvable,
57
- }: RouteurProps): ReactElement {
75
+ export function Router({
76
+ shell,
77
+ home,
78
+ about,
79
+ signIn,
80
+ register,
81
+ profile,
82
+ notFound,
83
+ url,
84
+ }: RouterProps): ReactElement {
58
85
  return (
59
- <Router>
86
+ <RouterProvider
87
+ history={url === undefined ? undefined : createMemoryHistory([url])}
88
+ >
60
89
  <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} />
90
+ {/* `Outlet` marks the place where the current page renders: that is
91
+ what keeps the navigation and the footer from being remounted on
92
+ every route change. */}
93
+ <Route path="/" element={shell(<Outlet />)}>
94
+ <Route index element={home} />
95
+ <Route path="about" element={about} />
96
+ <Route path="sign-in" element={signIn} />
97
+ <Route path="register" element={register} />
98
+ <Route path="profile" element={profile} />
99
+ <Route path="*" element={notFound} />
68
100
  </Route>
69
101
  </Routes>
70
- </Router>
102
+ </RouterProvider>
71
103
  )
72
104
  }
@@ -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,19 +1,22 @@
1
1
  import { defineConfig } from 'odoro'
2
2
 
3
3
  export default defineConfig({
4
- // Le client vit dans son propre dossier : la racine du moteur l'y suit.
4
+ // The client lives in its own directory: the engine root follows it there.
5
5
  root: 'client',
6
6
  alias: {
7
7
  '@': 'src',
8
8
  },
9
9
  build: {
10
- // Le serveur sert ce dossier en production.
10
+ // The server serves this directory in production.
11
11
  outDir: '../dist/client',
12
+ // Every route becomes a complete document. See
13
+ // `client/src/entry-server.tsx` for the list, and for how to do without it.
14
+ prerender: true,
12
15
  },
13
16
  server: {
14
17
  port: 5180,
15
- // Les appels d'API sont transmis au serveur, qui tourne en parallele :
16
- // le navigateur ne voit qu'une seule origine, donc aucune question de CORS.
18
+ // API calls are forwarded to the server, which runs alongside: the browser
19
+ // only ever sees one origin, so no CORS question arises.
17
20
  proxy: {
18
21
  '/api': 'http://localhost:3001',
19
22
  },
@@ -16,11 +16,13 @@
16
16
  "@odoro-cli/libs": "latest",
17
17
  "@odoro-cli/server": "latest",
18
18
  "express": "^5.1.0",
19
+ "pg": "^8.16.3",
19
20
  "react": "^19.2.0",
20
21
  "react-dom": "^19.2.0",
21
22
  "zod": "^4.5.1"
22
23
  },
23
24
  "devDependencies": {
25
+ "@types/pg": "^8.15.6",
24
26
  "@types/express": "^5.0.3",
25
27
  "@types/node": "^22.18.8",
26
28
  "@types/react": "^19.2.2",
@@ -1,21 +1,21 @@
1
1
  /**
2
- * Lance le client et le serveur en parallele.
2
+ * Runs the client and the server side by side.
3
3
  *
4
- * Un script maison plutot qu'une dependance : le besoin tient en cinquante
5
- * lignes, et il vaut mieux que la sortie des deux processus reste lisible et
6
- * qu'une interruption les arrete tous les deux proprement.
4
+ * A home-made script rather than a dependency: the need fits in fifty lines,
5
+ * and it is better that the output of both processes stays readable and that
6
+ * an interrupt stops both of them cleanly.
7
7
  */
8
8
 
9
9
  import { spawn } from 'node:child_process'
10
10
 
11
- const RESET = '\u001b[0m'
11
+ const RESET = ''
12
12
 
13
- /** Processus a lancer, avec leur etiquette et leur couleur. */
13
+ /** Processes to start, with their label and their colour. */
14
14
  const TASKS = [
15
- { label: 'client', color: '\u001b[35m', command: 'odoro', args: ['dev'] },
15
+ { label: 'client', color: '', command: 'odoro', args: ['dev'] },
16
16
  {
17
- label: 'serveur',
18
- color: '\u001b[36m',
17
+ label: 'server',
18
+ color: '',
19
19
  command: 'tsx',
20
20
  args: ['watch', 'server/src/main.ts'],
21
21
  },
@@ -23,7 +23,7 @@ const TASKS = [
23
23
 
24
24
  const children = []
25
25
 
26
- /** Prefixe chaque ligne de sortie par l'etiquette de son processus. */
26
+ /** Prefixes every output line with the label of its process. */
27
27
  function pipe(stream, label, color) {
28
28
  let buffer = ''
29
29
  stream.setEncoding('utf8')
@@ -37,7 +37,7 @@ function pipe(stream, label, color) {
37
37
  })
38
38
  }
39
39
 
40
- /** Arrete tous les processus encore vivants. */
40
+ /** Stops every process still alive. */
41
41
  function stopAll() {
42
42
  for (const child of children) {
43
43
  if (child.exitCode === null && !child.killed) child.kill()
@@ -47,7 +47,7 @@ function stopAll() {
47
47
  for (const task of TASKS) {
48
48
  const child = spawn(task.command, task.args, {
49
49
  stdio: ['inherit', 'pipe', 'pipe'],
50
- // Sous Windows, les binaires de node_modules sont des scripts shell.
50
+ // On Windows, the binaries in node_modules are shell scripts.
51
51
  shell: process.platform === 'win32',
52
52
  })
53
53
 
@@ -55,9 +55,9 @@ for (const task of TASKS) {
55
55
  pipe(child.stderr, task.label, task.color)
56
56
 
57
57
  child.on('exit', (code) => {
58
- // Si l'un s'arrete, l'autre n'a plus de raison de tourner.
58
+ // If one stops, the other has no reason left to run.
59
59
  if (code !== 0 && code !== null) {
60
- process.stderr.write(`[${task.label}] arret avec le code ${code}\n`)
60
+ process.stderr.write(`[${task.label}] stopped with code ${code}\n`)
61
61
  }
62
62
  stopAll()
63
63
  process.exitCode = code ?? 0
@@ -1,22 +1,21 @@
1
1
  /**
2
- * Le serveur applicatif : un assemblage de modules, rien de plus.
2
+ * The application server: an assembly of modules, nothing more.
3
3
  *
4
- * ## Ce fichier ne contient aucune logique
4
+ * ## This file holds no logic
5
5
  *
6
- * C'est voulu. Tout ce qu'une application fait vit dans un module, et ce
7
- * fichier ne fait que dire lesquels sont actifs. Activer ou desactiver une
8
- * fonctionnalite tient alors en une ligne, et rien d'autre ne bouge.
6
+ * That is deliberate. Everything an application does lives in a module, and
7
+ * this file does nothing but say which ones are active. Enabling or disabling
8
+ * a feature then takes one line, and nothing else moves.
9
9
  *
10
- * Un serveur ou les routes s'ajoutent directement ici finit par melanger
11
- * l'assemblage et le metier, et « desactiver l'authentification » devient un
12
- * travail d'archeologie plutot qu'une ligne commentee.
10
+ * A server where routes are added directly here ends up mixing the assembly
11
+ * with the domain, and "disable authentication" becomes a piece of archaeology
12
+ * rather than a commented line.
13
13
  *
14
- * ## En developpement et en production
14
+ * ## In development and in production
15
15
  *
16
- * En developpement, ce serveur n'expose que l'API : le client est servi par
17
- * Odoro, qui lui transmet les appels commencant par `/api`. En production, il
18
- * sert en plus le resultat de la compilation du client — une seule chose a
19
- * deployer.
16
+ * In development, this server exposes only the API: the client is served by
17
+ * Odoro, which forwards the calls starting with `/api` to it. In production,
18
+ * it also serves the result of the client build — a single thing to deploy.
20
19
  *
21
20
  * @module
22
21
  */
@@ -34,12 +33,20 @@ import {
34
33
  } from '@odoro-cli/server'
35
34
  import express from 'express'
36
35
 
36
+ import { createAuthModule } from './modules/auth/index.js'
37
37
  import { createHealthModule } from './modules/health/index.js'
38
38
 
39
- /** Racine du module compile, pour retrouver le client a cote. */
39
+ /** Root of the compiled module, to find the client next to it. */
40
40
  const HERE = dirname(fileURLToPath(import.meta.url))
41
41
 
42
- /** Assemble l'application. Exporte pour que les tests la montent sans l'ecouter. */
42
+ /**
43
+ * Assembles the application. Exported so that tests can mount it without
44
+ * listening.
45
+ *
46
+ * @returns `app` is the kernel and its modules; `express` is what has to be
47
+ * listened on — in production, a wrapper that serves the built client first.
48
+ * An API test mounts `app.express`; an end-to-end test, `express`.
49
+ */
43
50
  export function buildServer() {
44
51
  const config = loadConfig()
45
52
 
@@ -48,20 +55,26 @@ export function buildServer() {
48
55
  pretty: config.NODE_ENV === 'development',
49
56
  })
50
57
 
51
- // La configuration et le journal sont dans le conteneur : un module les y
52
- // trouve sans qu'on les lui passe de main en main a travers trois couches.
58
+ // The configuration and the logger are in the container: a module finds them
59
+ // there without their being handed down through three layers.
53
60
  const container = createContainer()
54
61
  .register('config', () => config)
55
62
  .register('logger', () => logger)
56
63
 
64
+ // The store comes back out: the guard reads it on every request, and the
65
+ // shutdown has to close its connections.
66
+ const auth = createAuthModule(config)
67
+
57
68
  const app = createApp({
58
69
  config,
59
70
  logger,
60
71
  container: container as never,
72
+ authenticate: auth.authenticate,
61
73
  modules: [
62
74
  createHealthModule(config),
63
- // Les modules du socle s'ajoutent ici, dans n'importe quel ordre :
64
- // le noyau les trie selon leurs dependances.
75
+ auth.module,
76
+ // The foundation modules are added here, in any order: the kernel sorts
77
+ // them according to their dependencies.
65
78
  //
66
79
  // authModule,
67
80
  // accountModule,
@@ -69,58 +82,119 @@ export function buildServer() {
69
82
  ],
70
83
  })
71
84
 
72
- if (config.NODE_ENV === 'production') {
73
- // `dist/server/main.js` -> `dist/client`
74
- const client = resolve(HERE, '..', 'client')
75
- if (existsSync(client)) {
76
- app.express.use(
77
- express.static(client, { index: false, maxAge: '1y', immutable: true }),
78
- )
79
- // Repli d'application monopage : toute route hors API rend le document,
80
- // et c'est le routeur client qui decide de la suite.
81
- app.express.get(/^(?!\/api\/).*/, (_request, response) => {
82
- response.sendFile(join(client, 'index.html'))
83
- })
85
+ // `dist/server/main.js` -> `dist/client`
86
+ const client = resolve(HERE, '..', 'client')
87
+ const servesTheClient = config.NODE_ENV === 'production' && existsSync(client)
88
+
89
+ if (!servesTheClient) return { app, express: app.express, config, logger, auth }
90
+
91
+ // ## Why one application wrapping another
92
+ //
93
+ // `createApp` ends by installing the 404 handler and the error handler.
94
+ // Anything mounted **after** it would therefore be unreachable: the request
95
+ // would already have its answer. The client was thus never served in
96
+ // production — with no error at startup, since the mounting succeeds; every
97
+ // page simply came back 404.
98
+ //
99
+ // So the wrapper handles the files first and only hands over to the API for
100
+ // what it has not served. The order is explicit, and the kernel stays free to
101
+ // end with its handlers.
102
+ const wrapper = express()
103
+ wrapper.set('trust proxy', true)
104
+ wrapper.disable('x-powered-by')
105
+
106
+ // `index: false`: without it, a directory would return its `index.html` with
107
+ // the cache header of a hashed file — one year, immutable —, and a wording
108
+ // fix would stay invisible for a year. Those documents go through the
109
+ // handling below, with no caching.
110
+ //
111
+ // `redirect: false`: prerendering drops one directory per route, and without
112
+ // it `/about` answered with a redirect to `/about/`. One more round trip,
113
+ // and above all an address that is not the one that was published.
114
+ wrapper.use(
115
+ express.static(client, {
116
+ index: false,
117
+ redirect: false,
118
+ maxAge: '1y',
119
+ immutable: true,
120
+ }),
121
+ )
122
+
123
+ // A prerendered route is served as it stands; everything else falls back to
124
+ // the document, and the client router decides what happens next.
125
+ //
126
+ // Without this lookup, `/about` would receive the document of the root: the
127
+ // page would end up showing, once the script had run, but the prerendered
128
+ // HTML — the one robots and link previews read — would be the home page's,
129
+ // with its title and its description.
130
+ wrapper.get(/^(?!\/api\/).*/, (request, response, next) => {
131
+ const route = request.path.replace(/^\/+|\/+$/g, '')
132
+ const prerendered = route === '' ? undefined : resolve(client, route, 'index.html')
133
+
134
+ if (
135
+ prerendered !== undefined &&
136
+ prerendered.startsWith(client) &&
137
+ existsSync(prerendered)
138
+ ) {
139
+ response.sendFile(prerendered)
140
+ return
84
141
  }
85
- }
86
142
 
87
- return { app, config, logger }
143
+ const document = join(client, 'index.html')
144
+ if (!existsSync(document)) {
145
+ // The client has not been built: the API will answer, and its 404 will
146
+ // at least say that no route matches.
147
+ next()
148
+ return
149
+ }
150
+ response.sendFile(document)
151
+ })
152
+
153
+ wrapper.use(app.express)
154
+
155
+ return { app, express: wrapper, config, logger, auth }
88
156
  }
89
157
 
90
- /** Demarre le serveur. */
158
+ /** Starts the server. */
91
159
  function main(): void {
92
160
  let server
93
161
  try {
94
162
  server = buildServer()
95
163
  } catch (cause) {
96
164
  if (cause instanceof ConfigError) {
97
- // La configuration est incomplete : le message liste tout ce qui manque,
98
- // d'un coup. Rien ne sert de demarrer a moitie.
165
+ // The configuration is incomplete: the message lists everything that is
166
+ // missing, at once. There is no point starting halfway.
99
167
  console.error(cause.message)
100
168
  process.exit(1)
101
169
  }
102
170
  throw cause
103
171
  }
104
172
 
105
- const { app, config, logger } = server
173
+ // `express` and not `app.express`: it is the wrapper that serves the client
174
+ // before handing over to the API. In development the two are the same thing.
175
+ const { express: application, config, logger, auth } = server
106
176
 
107
- const listener = app.express.listen(config.PORT, () => {
108
- logger.info({ port: config.PORT, environment: config.NODE_ENV }, 'serveur a l ecoute')
177
+ const listener = application.listen(config.PORT, () => {
178
+ logger.info({ port: config.PORT, environment: config.NODE_ENV }, 'server listening')
109
179
  })
110
180
 
111
- // Arret propre : on cesse d'accepter, on laisse finir ce qui est en cours, et
112
- // on abandonne au-dela du delai plutot que de rester suspendu.
181
+ // Graceful shutdown: stop accepting, let what is in flight finish, and give
182
+ // up past the deadline rather than hanging.
113
183
  const stop = (signal: string): void => {
114
- logger.info({ signal }, 'arret demande')
184
+ logger.info({ signal }, 'shutdown requested')
115
185
  const deadline = setTimeout(() => {
116
- logger.warn('delai depasse, arret force')
186
+ logger.warn('deadline exceeded, forcing shutdown')
117
187
  process.exit(1)
118
188
  }, config.SHUTDOWN_TIMEOUT)
119
189
  deadline.unref()
120
190
 
121
191
  listener.close(() => {
122
- clearTimeout(deadline)
123
- process.exit(0)
192
+ // The database connections go with the server. Left open, a hosted
193
+ // database counts them against the limit long after the process is gone.
194
+ void auth.store.dispose().finally(() => {
195
+ clearTimeout(deadline)
196
+ process.exit(0)
197
+ })
124
198
  })
125
199
  }
126
200