odoro 0.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.
Files changed (59) hide show
  1. package/LICENSE +12 -0
  2. package/client.d.ts +84 -0
  3. package/dist/build-SNTGJH2J.js +4 -0
  4. package/dist/chunk-2YDI5NKV.js +55 -0
  5. package/dist/chunk-G2WW7N4C.js +1872 -0
  6. package/dist/chunk-G5QXVBYT.js +1139 -0
  7. package/dist/chunk-JMEHF3KN.js +35 -0
  8. package/dist/chunk-PEMYUK2D.js +64 -0
  9. package/dist/chunk-T42X2NJN.js +98 -0
  10. package/dist/chunk-T6RLHSCW.js +123 -0
  11. package/dist/chunk-UGRSODHU.js +154 -0
  12. package/dist/cli.d.ts +40 -0
  13. package/dist/cli.js +236 -0
  14. package/dist/commands-FVAHVVVN.js +793 -0
  15. package/dist/commands-V44Y5G4F.js +245 -0
  16. package/dist/create-SH2M722Y.js +288 -0
  17. package/dist/index.d.ts +354 -0
  18. package/dist/index.js +7 -0
  19. package/dist/package-AUINPBEX.js +62 -0
  20. package/dist/preview-LOAO5Y6V.js +3 -0
  21. package/dist/registry/index.d.ts +316 -0
  22. package/dist/registry/index.js +2 -0
  23. package/dist/server-MZ76LPAG.js +3 -0
  24. package/package.json +57 -0
  25. package/templates/react-ts/README.md +52 -0
  26. package/templates/react-ts/_gitignore +8 -0
  27. package/templates/react-ts/index.html +13 -0
  28. package/templates/react-ts/odoro.config.ts +10 -0
  29. package/templates/react-ts/package.json +23 -0
  30. package/templates/react-ts/public/favicon.svg +4 -0
  31. package/templates/react-ts/src/App.tsx +66 -0
  32. package/templates/react-ts/src/main.tsx +18 -0
  33. package/templates/react-ts/src/odoro-env.d.ts +1 -0
  34. package/templates/react-ts/src/routes/About.tsx +19 -0
  35. package/templates/react-ts/src/routes/Home.tsx +80 -0
  36. package/templates/react-ts/src/routes/NotFound.tsx +14 -0
  37. package/templates/react-ts/src/styles.css +13 -0
  38. package/templates/react-ts/tsconfig.json +25 -0
  39. package/templates/react-ts-server/Dockerfile +51 -0
  40. package/templates/react-ts-server/README.md +87 -0
  41. package/templates/react-ts-server/_dockerignore +8 -0
  42. package/templates/react-ts-server/_env.example +78 -0
  43. package/templates/react-ts-server/_gitignore +8 -0
  44. package/templates/react-ts-server/client/index.html +13 -0
  45. package/templates/react-ts-server/client/public/favicon.svg +4 -0
  46. package/templates/react-ts-server/client/src/App.tsx +66 -0
  47. package/templates/react-ts-server/client/src/main.tsx +18 -0
  48. package/templates/react-ts-server/client/src/odoro-env.d.ts +1 -0
  49. package/templates/react-ts-server/client/src/routes/About.tsx +19 -0
  50. package/templates/react-ts-server/client/src/routes/Home.tsx +148 -0
  51. package/templates/react-ts-server/client/src/routes/NotFound.tsx +14 -0
  52. package/templates/react-ts-server/client/src/styles.css +13 -0
  53. package/templates/react-ts-server/odoro.config.ts +21 -0
  54. package/templates/react-ts-server/package.json +33 -0
  55. package/templates/react-ts-server/scripts/dev.mjs +74 -0
  56. package/templates/react-ts-server/server/src/main.ts +131 -0
  57. package/templates/react-ts-server/server/src/modules/health/index.ts +144 -0
  58. package/templates/react-ts-server/server/tsconfig.json +23 -0
  59. package/templates/react-ts-server/tsconfig.json +27 -0
@@ -0,0 +1,21 @@
1
+ import { defineConfig } from 'odoro'
2
+
3
+ export default defineConfig({
4
+ // Le client vit dans son propre dossier : la racine du moteur l'y suit.
5
+ root: 'client',
6
+ alias: {
7
+ '@': 'src',
8
+ },
9
+ build: {
10
+ // Le serveur sert ce dossier en production.
11
+ outDir: '../dist/client',
12
+ },
13
+ server: {
14
+ 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.
17
+ proxy: {
18
+ '/api': 'http://localhost:3001',
19
+ },
20
+ },
21
+ })
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "odoro-app",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "node scripts/dev.mjs",
8
+ "dev:client": "odoro dev",
9
+ "dev:server": "tsx watch server/src/main.ts",
10
+ "build": "odoro build && tsc -p server/tsconfig.json",
11
+ "start": "node dist/server/main.js",
12
+ "preview": "odoro preview",
13
+ "typecheck": "tsc --noEmit && tsc --noEmit -p server/tsconfig.json"
14
+ },
15
+ "dependencies": {
16
+ "@odoro-cli/libs": "^0.0.0",
17
+ "@odoro-cli/server": "^0.0.0",
18
+ "express": "^5.1.0",
19
+ "react": "^19.2.0",
20
+ "react-dom": "^19.2.0",
21
+ "zod": "^4.5.1"
22
+ },
23
+ "devDependencies": {
24
+ "@types/express": "^5.0.3",
25
+ "@types/node": "^22.18.8",
26
+ "@types/react": "^19.2.2",
27
+ "@types/react-dom": "^19.2.1",
28
+ "odoro": "^0.0.0",
29
+ "pino-pretty": "^13.0.0",
30
+ "tsx": "^4.20.6",
31
+ "typescript": "^5.9.3"
32
+ }
33
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Lance le client et le serveur en parallele.
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.
7
+ */
8
+
9
+ import { spawn } from 'node:child_process'
10
+
11
+ const RESET = '\u001b[0m'
12
+
13
+ /** Processus a lancer, avec leur etiquette et leur couleur. */
14
+ const TASKS = [
15
+ { label: 'client', color: '\u001b[35m', command: 'odoro', args: ['dev'] },
16
+ {
17
+ label: 'serveur',
18
+ color: '\u001b[36m',
19
+ command: 'tsx',
20
+ args: ['watch', 'server/src/main.ts'],
21
+ },
22
+ ]
23
+
24
+ const children = []
25
+
26
+ /** Prefixe chaque ligne de sortie par l'etiquette de son processus. */
27
+ function pipe(stream, label, color) {
28
+ let buffer = ''
29
+ stream.setEncoding('utf8')
30
+ stream.on('data', (chunk) => {
31
+ buffer += chunk
32
+ const lines = buffer.split('\n')
33
+ buffer = lines.pop() ?? ''
34
+ for (const line of lines) {
35
+ process.stdout.write(`${color}[${label}]${RESET} ${line}\n`)
36
+ }
37
+ })
38
+ }
39
+
40
+ /** Arrete tous les processus encore vivants. */
41
+ function stopAll() {
42
+ for (const child of children) {
43
+ if (child.exitCode === null && !child.killed) child.kill()
44
+ }
45
+ }
46
+
47
+ for (const task of TASKS) {
48
+ const child = spawn(task.command, task.args, {
49
+ stdio: ['inherit', 'pipe', 'pipe'],
50
+ // Sous Windows, les binaires de node_modules sont des scripts shell.
51
+ shell: process.platform === 'win32',
52
+ })
53
+
54
+ pipe(child.stdout, task.label, task.color)
55
+ pipe(child.stderr, task.label, task.color)
56
+
57
+ child.on('exit', (code) => {
58
+ // Si l'un s'arrete, l'autre n'a plus de raison de tourner.
59
+ if (code !== 0 && code !== null) {
60
+ process.stderr.write(`[${task.label}] arret avec le code ${code}\n`)
61
+ }
62
+ stopAll()
63
+ process.exitCode = code ?? 0
64
+ })
65
+
66
+ children.push(child)
67
+ }
68
+
69
+ for (const signal of ['SIGINT', 'SIGTERM']) {
70
+ process.on(signal, () => {
71
+ stopAll()
72
+ process.exit(0)
73
+ })
74
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Le serveur applicatif : un assemblage de modules, rien de plus.
3
+ *
4
+ * ## Ce fichier ne contient aucune logique
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.
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.
13
+ *
14
+ * ## En developpement et en production
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.
20
+ *
21
+ * @module
22
+ */
23
+
24
+ import { existsSync } from 'node:fs'
25
+ import { dirname, join, resolve } from 'node:path'
26
+ import { fileURLToPath } from 'node:url'
27
+
28
+ import {
29
+ ConfigError,
30
+ createApp,
31
+ createContainer,
32
+ createLogger,
33
+ loadConfig,
34
+ } from '@odoro-cli/server'
35
+ import express from 'express'
36
+
37
+ import { createHealthModule } from './modules/health/index.js'
38
+
39
+ /** Racine du module compile, pour retrouver le client a cote. */
40
+ const HERE = dirname(fileURLToPath(import.meta.url))
41
+
42
+ /** Assemble l'application. Exporte pour que les tests la montent sans l'ecouter. */
43
+ export function buildServer() {
44
+ const config = loadConfig()
45
+
46
+ const logger = createLogger({
47
+ level: config.LOG_LEVEL,
48
+ pretty: config.NODE_ENV === 'development',
49
+ })
50
+
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.
53
+ const container = createContainer()
54
+ .register('config', () => config)
55
+ .register('logger', () => logger)
56
+
57
+ const app = createApp({
58
+ config,
59
+ logger,
60
+ container: container as never,
61
+ modules: [
62
+ createHealthModule(config),
63
+ // Les modules du socle s'ajoutent ici, dans n'importe quel ordre :
64
+ // le noyau les trie selon leurs dependances.
65
+ //
66
+ // authModule,
67
+ // accountModule,
68
+ // settingsModule,
69
+ ],
70
+ })
71
+
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
+ })
84
+ }
85
+ }
86
+
87
+ return { app, config, logger }
88
+ }
89
+
90
+ /** Demarre le serveur. */
91
+ function main(): void {
92
+ let server
93
+ try {
94
+ server = buildServer()
95
+ } catch (cause) {
96
+ 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.
99
+ console.error(cause.message)
100
+ process.exit(1)
101
+ }
102
+ throw cause
103
+ }
104
+
105
+ const { app, config, logger } = server
106
+
107
+ const listener = app.express.listen(config.PORT, () => {
108
+ logger.info({ port: config.PORT, environment: config.NODE_ENV }, 'serveur a l ecoute')
109
+ })
110
+
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.
113
+ const stop = (signal: string): void => {
114
+ logger.info({ signal }, 'arret demande')
115
+ const deadline = setTimeout(() => {
116
+ logger.warn('delai depasse, arret force')
117
+ process.exit(1)
118
+ }, config.SHUTDOWN_TIMEOUT)
119
+ deadline.unref()
120
+
121
+ listener.close(() => {
122
+ clearTimeout(deadline)
123
+ process.exit(0)
124
+ })
125
+ }
126
+
127
+ process.on('SIGTERM', () => stop('SIGTERM'))
128
+ process.on('SIGINT', () => stop('SIGINT'))
129
+ }
130
+
131
+ main()
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Module de sante — et exemple de ce a quoi ressemble un module.
3
+ *
4
+ * C'est le seul module de ce template, et il est ecrit ici plutot que fourni
5
+ * par `@odoro-cli/server` pour une raison : ecrire un module est la premiere chose
6
+ * qu'on fait sur ce socle, et un exemple qu'on peut ouvrir vaut mieux qu'une
7
+ * page de documentation.
8
+ *
9
+ * ## Un module est une fonction de ce dont il a besoin
10
+ *
11
+ * Celui-ci recoit la configuration en parametre plutot que de lire
12
+ * `process.env`. C'est la regle du socle : l'environnement est valide une fois,
13
+ * au demarrage, et tout le reste consomme le resultat. Une lecture directe
14
+ * echappe a cette validation et se manifeste a la centieme requete.
15
+ *
16
+ * ## Deux points de controle, et pourquoi ils different
17
+ *
18
+ * `/api/health` repond que **le processus vit**. Il ne teste rien d'autre, et
19
+ * doit repondre meme quand tout le reste est casse : c'est ce qu'un
20
+ * orchestrateur interroge pour decider s'il faut redemarrer le conteneur. Le
21
+ * faire dependre de la base ferait redemarrer un serveur parfaitement sain
22
+ * chaque fois que la base hoquette — et un redemarrage ne repare pas une base.
23
+ *
24
+ * `/api/ready` repond que **le service peut travailler**, et rend 503 tant
25
+ * qu'il manque quelque chose : c'est ce qu'un repartiteur interroge pour
26
+ * decider s'il peut envoyer du trafic.
27
+ *
28
+ * Confondre les deux donne l'un des deux defauts : un service qui redemarre en
29
+ * boucle pendant un incident de base, ou un repartiteur qui envoie du trafic a
30
+ * un service incapable de repondre.
31
+ *
32
+ * ## Pourquoi `/ready` echoue au premier demarrage
33
+ *
34
+ * Un projet fraichement echafaude n'a pas encore de `DATABASE_URL`. Le client
35
+ * demarre, l'interface s'affiche, et `/api/ready` dit ce qui manque. On voit
36
+ * donc quelque chose des la premiere minute, et on sait exactement ce qu'il
37
+ * reste a faire.
38
+ *
39
+ * @module
40
+ */
41
+
42
+ import {
43
+ ServiceUnavailableError,
44
+ defineModule,
45
+ route,
46
+ type KernelConfig,
47
+ } from '@odoro-cli/server'
48
+ import { z } from 'zod'
49
+
50
+ /** Ce que rend le controle de vie. */
51
+ const liveness = z.object({
52
+ status: z.literal('ok'),
53
+ environment: z.string(),
54
+ uptime: z.number(),
55
+ })
56
+
57
+ /** Ce que rend le controle de disponibilite, quand tout repond. */
58
+ const readiness = z.object({
59
+ ready: z.literal(true),
60
+ dependencies: z.array(z.object({ name: z.string(), detail: z.string() })),
61
+ })
62
+
63
+ /** Une dependance et son etat. */
64
+ interface Dependency {
65
+ readonly name: string
66
+ readonly ready: boolean
67
+ readonly detail: string
68
+ }
69
+
70
+ /**
71
+ * Etat des dependances.
72
+ *
73
+ * La base n'est pas encore interrogee — le socle n'a pas sa couche de
74
+ * persistance. Ce qui est verifie ici est sa **configuration**, ce qui suffit
75
+ * a distinguer un projet qui n'a jamais recu d'URL d'un projet configure.
76
+ * Quand la persistance arrivera, c'est cette fonction qui apprendra a ouvrir
77
+ * une connexion, et rien d'autre ne changera.
78
+ */
79
+ function inspect(config: KernelConfig): readonly Dependency[] {
80
+ const url = config.DATABASE_URL.trim()
81
+ return [
82
+ {
83
+ name: 'database',
84
+ ready: url.length > 0,
85
+ detail:
86
+ url.length > 0
87
+ ? 'URL configuree'
88
+ : 'DATABASE_URL absente — voir .env.example, ou lancer `odoro db:create`',
89
+ },
90
+ ]
91
+ }
92
+
93
+ /**
94
+ * Construit le module.
95
+ *
96
+ * @example
97
+ * createApp({ modules: [createHealthModule(config)], … })
98
+ */
99
+ export function createHealthModule(config: KernelConfig) {
100
+ return defineModule({
101
+ name: 'health',
102
+ routes: [
103
+ route({
104
+ name: 'health.live',
105
+ method: 'GET',
106
+ path: '/api/health',
107
+ auth: 'public',
108
+ summary: 'Le processus repond.',
109
+ output: liveness,
110
+ handler: () => ({
111
+ status: 'ok' as const,
112
+ environment: config.NODE_ENV,
113
+ uptime: Math.round(process.uptime()),
114
+ }),
115
+ }),
116
+
117
+ route({
118
+ name: 'health.ready',
119
+ method: 'GET',
120
+ path: '/api/ready',
121
+ auth: 'public',
122
+ summary: 'Les dependances repondent.',
123
+ output: readiness,
124
+ handler: () => {
125
+ const dependencies = inspect(config)
126
+ const manquantes = dependencies.filter((d) => !d.ready)
127
+
128
+ if (manquantes.length > 0) {
129
+ // 503 et non 500 : la demande etait valide, c'est le service qui
130
+ // ne peut pas encore y repondre.
131
+ throw new ServiceUnavailableError(
132
+ manquantes.map((d) => `${d.name} — ${d.detail}`).join(' ; '),
133
+ )
134
+ }
135
+
136
+ return {
137
+ ready: true as const,
138
+ dependencies: dependencies.map(({ name, detail }) => ({ name, detail })),
139
+ }
140
+ },
141
+ }),
142
+ ] as never,
143
+ })
144
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023"],
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "moduleDetection": "force",
8
+ "verbatimModuleSyntax": true,
9
+ "types": ["node"],
10
+
11
+ "strict": true,
12
+ "noUncheckedIndexedAccess": true,
13
+ "noUnusedLocals": true,
14
+ "noUnusedParameters": true,
15
+ "noFallthroughCasesInSwitch": true,
16
+
17
+ "skipLibCheck": true,
18
+ "outDir": "../dist/server",
19
+ "rootDir": "./src",
20
+ "sourceMap": true
21
+ },
22
+ "include": ["src"]
23
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "verbatimModuleSyntax": true,
12
+ "noEmit": true,
13
+
14
+ "strict": true,
15
+ "noUncheckedIndexedAccess": true,
16
+ "noUnusedLocals": true,
17
+ "noUnusedParameters": true,
18
+ "noFallthroughCasesInSwitch": true,
19
+
20
+ "skipLibCheck": true,
21
+ "baseUrl": ".",
22
+ "paths": {
23
+ "@/*": ["./client/src/*"]
24
+ }
25
+ },
26
+ "include": ["client/src", "odoro.config.ts"]
27
+ }