odoro 1.0.9 → 2.1.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.
- package/README.md +94 -0
- package/client.d.ts +90 -16
- package/dist/build-XJIP4GNT.js +5 -0
- package/dist/{chunk-34RGOZFA.js → chunk-4D56Z7G3.js} +49 -50
- package/dist/{chunk-JMEHF3KN.js → chunk-7ZB5MAM6.js} +4 -4
- package/dist/chunk-DF67LADH.js +735 -0
- package/dist/{chunk-LHZGX5ML.js → chunk-HS7NGZLM.js} +307 -196
- package/dist/chunk-LIKVHWB4.js +231 -0
- package/dist/chunk-QOVBLN7A.js +241 -0
- package/dist/chunk-RTWJ5GFB.js +729 -0
- package/dist/chunk-TONYIKCN.js +57 -0
- package/dist/cli.d.ts +12 -13
- package/dist/cli.js +86 -63
- package/dist/{commands-AHMXBWQQ.js → commands-GYA7NTCX.js} +124 -124
- package/dist/commands-UOWN3IK4.js +181 -0
- package/dist/{create-CYVSVDAD.js → create-B2TLV6XI.js} +198 -160
- package/dist/index.d.ts +603 -151
- package/dist/index.js +6 -6
- package/dist/{package-EIDLKAA6.js → package-3ZJOEXM4.js} +6 -5
- package/dist/preview-3IRG7MHV.js +4 -0
- package/dist/registry/index.d.ts +75 -76
- package/dist/registry/index.js +1 -1
- package/dist/{server-4UGN3SFS.js → server-ZB3P3EGS.js} +3 -3
- package/package.json +6 -5
- package/templates/react-ts/README.md +71 -35
- package/templates/react-ts/_env.example +9 -0
- package/templates/react-ts/_variants/with-engine/src/background.tsx +118 -0
- package/templates/react-ts/_variants/without-libs/src/App.tsx +380 -0
- package/templates/react-ts/_variants/without-libs/src/background.tsx +24 -0
- package/templates/react-ts/_variants/without-libs/src/entry-server.tsx +39 -0
- package/templates/react-ts/_variants/without-libs/src/main.tsx +26 -0
- package/templates/{react-ts-server/_variantes/sans-libs/client → react-ts/_variants/without-libs}/src/styles.css +137 -137
- package/templates/react-ts/{_variantes/sans-routeur → _variants/without-router}/src/App.tsx +141 -141
- package/templates/react-ts/_variants/without-router/src/entry-server.tsx +39 -0
- package/templates/react-ts/index.html +4 -4
- package/templates/react-ts/odoro.config.ts +5 -0
- package/templates/react-ts/src/App.tsx +219 -209
- package/templates/react-ts/src/background.tsx +66 -0
- package/templates/react-ts/src/entry-server.tsx +85 -0
- package/templates/react-ts/src/main.tsx +13 -4
- package/templates/react-ts/src/router.tsx +65 -45
- package/templates/react-ts/src/styles.css +5 -5
- package/templates/react-ts-server/Dockerfile +9 -9
- package/templates/react-ts-server/README.md +97 -45
- package/templates/react-ts-server/_env.example +31 -33
- package/templates/react-ts-server/_variants/with-engine/client/src/background.tsx +118 -0
- package/templates/react-ts-server/_variants/without-libs/client/src/App.tsx +380 -0
- package/templates/react-ts-server/_variants/without-libs/client/src/background.tsx +24 -0
- package/templates/react-ts-server/_variants/without-libs/client/src/entry-server.tsx +39 -0
- package/templates/react-ts-server/_variants/without-libs/client/src/main.tsx +26 -0
- package/templates/{react-ts/_variantes/sans-libs → react-ts-server/_variants/without-libs/client}/src/styles.css +137 -137
- package/templates/react-ts-server/{_variantes/sans-routeur → _variants/without-router}/client/src/App.tsx +141 -141
- package/templates/react-ts-server/_variants/without-router/client/src/entry-server.tsx +39 -0
- package/templates/react-ts-server/client/index.html +4 -4
- package/templates/react-ts-server/client/src/App.tsx +228 -210
- package/templates/react-ts-server/client/src/account.tsx +267 -0
- package/templates/react-ts-server/client/src/auth.tsx +139 -0
- package/templates/react-ts-server/client/src/background.tsx +66 -0
- package/templates/react-ts-server/client/src/entry-server.tsx +92 -0
- package/templates/react-ts-server/client/src/main.tsx +17 -5
- package/templates/react-ts-server/client/src/router.tsx +77 -45
- package/templates/react-ts-server/client/src/styles.css +5 -5
- package/templates/react-ts-server/odoro.config.ts +7 -4
- package/templates/react-ts-server/package.json +2 -0
- package/templates/react-ts-server/scripts/dev.mjs +14 -14
- package/templates/react-ts-server/server/src/main.ts +119 -45
- package/templates/react-ts-server/server/src/modules/auth/index.ts +238 -0
- package/templates/react-ts-server/server/src/modules/auth/password.ts +122 -0
- package/templates/react-ts-server/server/src/modules/auth/store.ts +193 -0
- package/templates/react-ts-server/server/src/modules/health/index.ts +45 -46
- package/dist/build-JFQHODAT.js +0 -5
- package/dist/chunk-22KJTV2R.js +0 -380
- package/dist/chunk-3SZIN6VG.js +0 -64
- package/dist/chunk-DL3NPC4H.js +0 -113
- package/dist/chunk-TQUJ3MFS.js +0 -268
- package/dist/chunk-ZVL7EXJO.js +0 -66
- package/dist/commands-4JRBD55Z.js +0 -245
- package/dist/preview-7DKEAQOQ.js +0 -4
- package/templates/react-ts/_variantes/avec-moteur/src/fond.tsx +0 -118
- package/templates/react-ts/_variantes/sans-libs/src/App.tsx +0 -382
- package/templates/react-ts/_variantes/sans-libs/src/fond.tsx +0 -23
- package/templates/react-ts/_variantes/sans-libs/src/main.tsx +0 -17
- package/templates/react-ts/src/fond.tsx +0 -66
- package/templates/react-ts-server/_variantes/avec-moteur/client/src/fond.tsx +0 -118
- package/templates/react-ts-server/_variantes/sans-libs/client/src/App.tsx +0 -382
- package/templates/react-ts-server/_variantes/sans-libs/client/src/fond.tsx +0 -23
- package/templates/react-ts-server/_variantes/sans-libs/client/src/main.tsx +0 -17
- package/templates/react-ts-server/client/src/fond.tsx +0 -66
|
@@ -1,72 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The router of the project.
|
|
3
3
|
*
|
|
4
|
-
* ##
|
|
4
|
+
* ## Why it lives alone, in its own file
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
* ##
|
|
10
|
+
* ## Why the pages are passed in, and not imported
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
18
|
-
* `App.tsx`
|
|
19
|
-
*
|
|
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 {
|
|
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
|
-
/**
|
|
30
|
-
export interface
|
|
31
|
-
/**
|
|
32
|
-
readonly
|
|
33
|
-
/**
|
|
34
|
-
readonly
|
|
35
|
-
/**
|
|
36
|
-
readonly
|
|
37
|
-
/**
|
|
38
|
-
readonly
|
|
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
|
-
*
|
|
65
|
+
* The route table.
|
|
43
66
|
*
|
|
44
67
|
* @example
|
|
45
|
-
* <
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
68
|
+
* <Router
|
|
69
|
+
* shell={(content) => <Shell>{content}</Shell>}
|
|
70
|
+
* home={<Home />}
|
|
71
|
+
* about={<About />}
|
|
72
|
+
* notFound={<NotFound />}
|
|
50
73
|
* />
|
|
51
74
|
*/
|
|
52
|
-
export function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
<
|
|
86
|
+
<RouterProvider
|
|
87
|
+
history={url === undefined ? undefined : createMemoryHistory([url])}
|
|
88
|
+
>
|
|
60
89
|
<Routes>
|
|
61
|
-
{/* `Outlet`
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
<Route path="/" element={
|
|
65
|
-
<Route index element={
|
|
66
|
-
<Route path="
|
|
67
|
-
<Route path="
|
|
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
|
-
</
|
|
102
|
+
</RouterProvider>
|
|
71
103
|
)
|
|
72
104
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
/* Styles
|
|
1
|
+
/* Styles that belong to the application.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
page,
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
16
|
-
//
|
|
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
|
-
*
|
|
2
|
+
* Runs the client and the server side by side.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 = '
|
|
11
|
+
const RESET = '[0m'
|
|
12
12
|
|
|
13
|
-
/**
|
|
13
|
+
/** Processes to start, with their label and their colour. */
|
|
14
14
|
const TASKS = [
|
|
15
|
-
{ label: 'client', color: '
|
|
15
|
+
{ label: 'client', color: '[35m', command: 'odoro', args: ['dev'] },
|
|
16
16
|
{
|
|
17
|
-
label: '
|
|
18
|
-
color: '
|
|
17
|
+
label: 'server',
|
|
18
|
+
color: '[36m',
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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}]
|
|
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
|
-
*
|
|
2
|
+
* The application server: an assembly of modules, nothing more.
|
|
3
3
|
*
|
|
4
|
-
* ##
|
|
4
|
+
* ## This file holds no logic
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
-
* ##
|
|
14
|
+
* ## In development and in production
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
* Odoro,
|
|
18
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
52
|
-
//
|
|
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
|
-
|
|
64
|
-
//
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
98
|
-
//
|
|
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
|
-
|
|
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 =
|
|
108
|
-
logger.info({ port: config.PORT, environment: config.NODE_ENV }, '
|
|
177
|
+
const listener = application.listen(config.PORT, () => {
|
|
178
|
+
logger.info({ port: config.PORT, environment: config.NODE_ENV }, 'server listening')
|
|
109
179
|
})
|
|
110
180
|
|
|
111
|
-
//
|
|
112
|
-
//
|
|
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 }, '
|
|
184
|
+
logger.info({ signal }, 'shutdown requested')
|
|
115
185
|
const deadline = setTimeout(() => {
|
|
116
|
-
logger.warn('
|
|
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
|
-
|
|
123
|
-
process.
|
|
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
|
|