@nodefony/documentation 10.0.0-alpha.1
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/LICENSE +544 -0
- package/README.md +168 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
- package/dist/index.js +75 -0
- package/dist/nodefony/config/config.js +71 -0
- package/dist/nodefony/config/defineModuleConfig.js +49 -0
- package/dist/nodefony/controller/DocumentationController.js +102 -0
- package/dist/nodefony/interfaces/IDocumentation.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/service/DocumentationService.js +421 -0
- package/dist/nodefony/src/docScanner.js +58 -0
- package/dist/nodefony/src/errors/DocumentationError.js +45 -0
- package/dist/nodefony/src/frontmatter.js +63 -0
- package/dist/nodefony/src/linkResolver.js +68 -0
- package/dist/nodefony/src/search.js +120 -0
- package/dist/nodefony/src/slug.js +62 -0
- package/dist/types/index.d.ts +58 -0
- package/dist/types/nodefony/config/config.d.ts +37 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +17 -0
- package/dist/types/nodefony/controller/DocumentationController.d.ts +33 -0
- package/dist/types/nodefony/interfaces/IDocumentation.d.ts +144 -0
- package/dist/types/nodefony/interfaces/index.d.ts +5 -0
- package/dist/types/nodefony/service/DocumentationService.d.ts +78 -0
- package/dist/types/nodefony/src/docScanner.d.ts +47 -0
- package/dist/types/nodefony/src/errors/DocumentationError.d.ts +32 -0
- package/dist/types/nodefony/src/frontmatter.d.ts +37 -0
- package/dist/types/nodefony/src/linkResolver.d.ts +55 -0
- package/dist/types/nodefony/src/search.d.ts +64 -0
- package/dist/types/nodefony/src/slug.d.ts +48 -0
- package/docs/architecture.md +647 -0
- package/docs/index.md +369 -0
- package/package.json +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# @nodefony/documentation
|
|
2
|
+
|
|
3
|
+
**Data plane de documentation transverse de Nodefony** — un module _headless_ (back pur) qui indexe
|
|
4
|
+
toute la documentation du projet, résout des variables dynamiques côté serveur, et l'expose en JSON
|
|
5
|
+
sous `/nodefony/documentation/api/*`.
|
|
6
|
+
|
|
7
|
+
Il ne rend **aucune page HTML**. Le rendu est laissé au consommateur : le front Studio (page React),
|
|
8
|
+
un générateur de site statique, ou le pipeline RAG. Le module se contente de fournir un **index** et
|
|
9
|
+
le **contenu résolu** des pages.
|
|
10
|
+
|
|
11
|
+
## Ce qu'il indexe
|
|
12
|
+
|
|
13
|
+
Conformément à [ADR-0001](https://github.com/nodefony/nodefony-core/blob/claude-ts/docs/adr/0001-docs-modules-emplacement-hybride.md) (emplacement
|
|
14
|
+
hybride), la doc vit à deux endroits, et les deux sont scannés :
|
|
15
|
+
|
|
16
|
+
1. **`docs/` racine** — la doc transverse, qui n'appartient à aucun module (guides, ADR, audits, releases).
|
|
17
|
+
2. **`<module>/docs/*.md`** — la doc co-localisée à chaque module (`src/packages/@nodefony/<m>/docs/`,
|
|
18
|
+
`src/nodefony/docs/`). Activable/désactivable via `scan.includeModules`.
|
|
19
|
+
|
|
20
|
+
## Installation / activation
|
|
21
|
+
|
|
22
|
+
Le module est déclaré dans les `@modules()` de l'application, **après** `@nodefony/framework`
|
|
23
|
+
(dépendance des décorateurs `@controller`) et **avant** `@nodefony/studio` (dont le front consomme ce
|
|
24
|
+
data plane) :
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
// index.ts (racine app)
|
|
28
|
+
@modules([
|
|
29
|
+
// …
|
|
30
|
+
"@nodefony/documentation",
|
|
31
|
+
"@nodefony/studio",
|
|
32
|
+
])
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
|
|
37
|
+
Surcharge depuis la config applicative sous la clé `module-documentation` (fusion récursive) :
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// src/modules/app/nodefony/config/config.ts
|
|
41
|
+
export default {
|
|
42
|
+
"module-documentation": {
|
|
43
|
+
scan: { includeModules: false }, // racine seule
|
|
44
|
+
repo: { url: "https://github.com/acme/app", editPathPrefix: "blob" },
|
|
45
|
+
cache: { ttlMs: 0 }, // rescan à chaque requête (dev)
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
<!-- prettier-ignore -->
|
|
51
|
+
| Option | Défaut | Rôle |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| `enabled` | `true` | Active le data plane au boot |
|
|
54
|
+
| `scan.rootDir` | `"docs"` | Dossier de doc transverse, relatif à `kernel.path` |
|
|
55
|
+
| `scan.includeModules` | `true` | Scanne aussi les `<module>/docs/*.md` |
|
|
56
|
+
| `scan.exclude` | `["session-retros","node_modules","dist"]` | Segments de chemin ignorés |
|
|
57
|
+
| `repo.url` | dépôt nodefony-core | Base du lien « Modifier sur GitHub » (URL publique) |
|
|
58
|
+
| `repo.branch` | _(auto)_ | Branche du lien ; si omise → branche git réelle (`GitService`) |
|
|
59
|
+
| `repo.editPathPrefix` | `"edit"` | Segment GitHub : `edit` / `blob` / `tree` |
|
|
60
|
+
| `cache.ttlMs` | `30000` | TTL (ms) du cache de l'**index** ; `0` = pas de cache |
|
|
61
|
+
|
|
62
|
+
**Variables d'environnement** (précédence maximale, utiles en CI/conteneur sans `.git`) :
|
|
63
|
+
`DOCS_REPO_URL`, `DOCS_REPO_BRANCH`.
|
|
64
|
+
|
|
65
|
+
## API HTTP
|
|
66
|
+
|
|
67
|
+
### `GET /nodefony/documentation/api/tree`
|
|
68
|
+
|
|
69
|
+
Index transverse — sections (par dossier racine, et par module) → pages, taguées par audience.
|
|
70
|
+
|
|
71
|
+
```jsonc
|
|
72
|
+
{
|
|
73
|
+
"generatedAt": "2026-05-31T…",
|
|
74
|
+
"audiences": [{ "key": "developer", "label": "Développeur", "desc": "…" }, …],
|
|
75
|
+
"sections": [
|
|
76
|
+
{ "id": "root-guides", "label": "Guides", "pages": [{ "slug": "root~guides~intro", "title": "Intro", "audience": [] }] },
|
|
77
|
+
{ "id": "mod-http", "label": "Module @nodefony/http", "module": "@nodefony/http", "pages": [ … ] }
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `GET /nodefony/documentation/api/page/{slug}`
|
|
83
|
+
|
|
84
|
+
Contenu d'une page : markdown sans frontmatter, variables `{{ }}` résolues, lien source assemblé.
|
|
85
|
+
|
|
86
|
+
```jsonc
|
|
87
|
+
{
|
|
88
|
+
"slug": "mod~http~index",
|
|
89
|
+
"title": "…",
|
|
90
|
+
"version": "10.0.0",
|
|
91
|
+
"status": "stable",
|
|
92
|
+
"updated": "2026-05-31",
|
|
93
|
+
"source": "src/packages/@nodefony/http/docs/index.md",
|
|
94
|
+
"sourceUrl": "https://github.com/nodefony/nodefony-core/edit/claude-ts/…",
|
|
95
|
+
"markdown": "# …",
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Slug inconnu ou non sûr → **404** avec un corps générique (`{ "slug": "…", "error": "Document inconnu." }`).
|
|
100
|
+
|
|
101
|
+
## Schéma de slug
|
|
102
|
+
|
|
103
|
+
Le slug est un identifiant **URL-safe sur un seul segment** (`{slug}` dans la route). C'est une **clé
|
|
104
|
+
d'allowlist**, jamais un chemin de fichier.
|
|
105
|
+
|
|
106
|
+
| Source | Exemple de fichier | Slug |
|
|
107
|
+
| ------ | -------------------------------- | ----------------------------- |
|
|
108
|
+
| Racine | `docs/guides/session-storage.md` | `root~guides~session-storage` |
|
|
109
|
+
| Module | `@nodefony/http/docs/index.md` | `mod~http~index` |
|
|
110
|
+
|
|
111
|
+
Le `/` devient `~` ; le scope npm (`@nodefony/`) est retiré.
|
|
112
|
+
|
|
113
|
+
## Frontmatter supporté
|
|
114
|
+
|
|
115
|
+
Bloc optionnel en tête de fichier, encadré de `---`. YAML **plat** uniquement (clé → scalaire ou liste) :
|
|
116
|
+
|
|
117
|
+
```markdown
|
|
118
|
+
---
|
|
119
|
+
title: La Socket Nodefony
|
|
120
|
+
audience: [developer, devops]
|
|
121
|
+
version: "10.0.0"
|
|
122
|
+
status: stable
|
|
123
|
+
updated: 2026-05-31
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
# Contenu…
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Champs lus : `title`, `audience` (parmi `developer`/`devops`/`supervisor`/`admin`), `version`,
|
|
130
|
+
`status` (`stable`/`draft`/`temporary`/`experimental`/`deprecated`), `updated`, `source`.
|
|
131
|
+
Non supporté (volontaire) : objets imbriqués, blocs multilignes `|`/`>`, ancres YAML.
|
|
132
|
+
|
|
133
|
+
## Variables dynamiques `{{ }}`
|
|
134
|
+
|
|
135
|
+
Le serveur remplace `{{ name }}` dans le markdown par la valeur d'un fournisseur enregistré. Built-in :
|
|
136
|
+
`{{ version }}`, `{{ branch }}`, `{{ commit }}`. Un nom inconnu est laissé tel quel (signale à l'auteur
|
|
137
|
+
qu'il manque un provider). Enregistrer une variable :
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
documentationService.registerVar("rps", () => String(computeRps()));
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
> ⚠️ Un provider doit retourner une valeur **sûre** (publique, dérivée) — jamais un secret ni un
|
|
144
|
+
> chemin FS absolu.
|
|
145
|
+
|
|
146
|
+
## Sécurité
|
|
147
|
+
|
|
148
|
+
- Le slug est validé (`isSafeSlug`) **avant** toute recherche ou lecture : rejet de `..`, `/`, `\`,
|
|
149
|
+
octet nul, charset non autorisé, longueur > 512.
|
|
150
|
+
- La lecture se fait toujours sur le **chemin réel** mémorisé au scan, jamais reconstruit depuis le slug.
|
|
151
|
+
- Les erreurs renvoient un message **générique** au client ; le détail est journalisé côté serveur.
|
|
152
|
+
|
|
153
|
+
## Briques pures réutilisables
|
|
154
|
+
|
|
155
|
+
Exportées pour un futur générateur de site statique ou le RAG (testées unitairement) :
|
|
156
|
+
`parseFrontmatter`, `metaString`, `metaList`, `scanDocsDir`, `isSafeSlug`, `pathToSlug`.
|
|
157
|
+
|
|
158
|
+
## Développement
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
cd src/packages/@nodefony/documentation
|
|
162
|
+
npm test # vitest — briques pures (frontmatter / slug / docScanner)
|
|
163
|
+
npm run build # rolldown + tsgo → dist/ + dist/types
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Licence
|
|
167
|
+
|
|
168
|
+
CeCILL-B — Christophe CAMENSULI.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
|
|
2
|
+
function __decorate(decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { __decorate as default };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorateMetadata.js
|
|
2
|
+
function __decorateMetadata(k, v) {
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
export { __decorateMetadata as default };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import config, { documentationConfigSchema } from "./nodefony/config/config.js";
|
|
2
|
+
import { defineDocumentationConfig, documentationConfigJsonSchema } from "./nodefony/config/defineModuleConfig.js";
|
|
3
|
+
import { metaList, metaString, parseFrontmatter } from "./nodefony/src/frontmatter.js";
|
|
4
|
+
import { isSafeSlug, pathToSlug } from "./nodefony/src/slug.js";
|
|
5
|
+
import { scanDocsDir } from "./nodefony/src/docScanner.js";
|
|
6
|
+
import { rewriteInternalLinks } from "./nodefony/src/linkResolver.js";
|
|
7
|
+
import { extractSearchText, foldText, searchDocs, splitSearchTerms } from "./nodefony/src/search.js";
|
|
8
|
+
import { DocNotFoundError, DocUnsafeSlugError, DocumentationError } from "./nodefony/src/errors/DocumentationError.js";
|
|
9
|
+
import DocumentationService, { ROOT_GROUPS, ROOT_PAGES } from "./nodefony/service/DocumentationService.js";
|
|
10
|
+
import __decorateMetadata from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
|
|
11
|
+
import __decorate from "./_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
|
|
12
|
+
import DocumentationController_default from "./nodefony/controller/DocumentationController.js";
|
|
13
|
+
import { GitService, Kernel, Module, services } from "nodefony";
|
|
14
|
+
import { controllers } from "@nodefony/framework";
|
|
15
|
+
//#region index.ts
|
|
16
|
+
/**
|
|
17
|
+
* @nodefony/documentation — Data plane de documentation transverse de Nodefony.
|
|
18
|
+
*
|
|
19
|
+
* Module **headless** (back pur) : il indexe la documentation co-localisée
|
|
20
|
+
* (`docs/` racine transverse + `<module>/docs/*.md`, ADR-0001), résout les
|
|
21
|
+
* variables dynamiques `{{ }}` côté serveur, et expose le tout sous
|
|
22
|
+
* `/nodefony/documentation/api/*`. Il ne rend AUCUN HTML — le front Studio (et,
|
|
23
|
+
* demain, un générateur de site statique ou le RAG P12) consomme ce data plane.
|
|
24
|
+
*
|
|
25
|
+
* Pourquoi un module dédié (et pas un simple controller Studio) : la doc porte
|
|
26
|
+
* de l'**état** (index scanné, cache TTL, registre de providers `{{ }}`) → un
|
|
27
|
+
* cycle de vie propre, hors hot path request (tout lazy). Cf
|
|
28
|
+
* [[project_doc_portal_faisabilite]].
|
|
29
|
+
*
|
|
30
|
+
* Voir aussi : CLAUDE.md (décisions figées), MEMORY.md (internals IA),
|
|
31
|
+
* README.md (usage humain), docs/ (doc vulgarisée surfacée dans Studio).
|
|
32
|
+
*/
|
|
33
|
+
let Documentation = class Documentation extends Module {
|
|
34
|
+
/** Module optionnel : un échec de son boot ne tue jamais le process (résilience Ph.3). */
|
|
35
|
+
static critical = false;
|
|
36
|
+
constructor(kernel) {
|
|
37
|
+
super("documentation", kernel, import.meta.url, config);
|
|
38
|
+
}
|
|
39
|
+
/** JSON Schema de la config documentation → data plane admin (config riche Studio). */
|
|
40
|
+
configSchema() {
|
|
41
|
+
return documentationConfigJsonSchema();
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Phase `onRegister` : valide la config (défauts + override `module-documentation`
|
|
45
|
+
* + env) via `defineDocumentationConfig`, puis la ré-assigne à `this.options`
|
|
46
|
+
* AVANT l'instanciation du `@services` (phase `onBoot`). Plante propre avec
|
|
47
|
+
* messages clairs si la config est invalide (convention Zod figée 2026-05-28).
|
|
48
|
+
*/
|
|
49
|
+
async onKernelRegister() {
|
|
50
|
+
this.options = defineDocumentationConfig(this.options);
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Phase `onReady` : enregistre les fournisseurs de variables `{{ }}` built-in
|
|
55
|
+
* sur le service (tous les modules sont alors bootés). Sources SÛRES seulement
|
|
56
|
+
* (version, identité git) — jamais de secret ni de chemin FS absolu.
|
|
57
|
+
*/
|
|
58
|
+
async onKernelReady() {
|
|
59
|
+
const svc = this.get("documentation");
|
|
60
|
+
if (!svc) return this;
|
|
61
|
+
const root = this.kernel?.path;
|
|
62
|
+
svc.registerVar("version", () => this.kernel?.version ?? "");
|
|
63
|
+
svc.registerVar("branch", () => GitService.branch(root));
|
|
64
|
+
svc.registerVar("commit", () => GitService.read(root).commit);
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
Documentation = __decorate([
|
|
69
|
+
services([DocumentationService]),
|
|
70
|
+
controllers([DocumentationController_default]),
|
|
71
|
+
__decorateMetadata("design:paramtypes", [typeof Kernel === "undefined" ? Object : Kernel])
|
|
72
|
+
], Documentation);
|
|
73
|
+
var documentation_default = Documentation;
|
|
74
|
+
//#endregion
|
|
75
|
+
export { DocNotFoundError, DocUnsafeSlugError, DocumentationController_default as DocumentationController, DocumentationError, DocumentationService, ROOT_GROUPS, ROOT_PAGES, documentation_default as default, defineDocumentationConfig, documentationConfigJsonSchema, documentationConfigSchema, extractSearchText, foldText, isSafeSlug, metaList, metaString, parseFrontmatter, pathToSlug, rewriteInternalLinks, scanDocsDir, searchDocs, splitSearchTerms };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region nodefony/config/config.ts
|
|
3
|
+
/**
|
|
4
|
+
* @nodefony/documentation — CONFIGURATION DU MODULE (schéma Zod = source unique).
|
|
5
|
+
*
|
|
6
|
+
* ⭐ TL;DR : CE SCHÉMA EST LA CONFIG. Chaque `.default(...)` = la valeur d'usine ;
|
|
7
|
+
* changer un défaut du module = ÉDITER ICI (et nulle part ailleurs). L'app, elle,
|
|
8
|
+
* surcharge via `use("@nodefony/...", { … })` dans SON `nodefony.config.ts`.
|
|
9
|
+
*
|
|
10
|
+
* RÈGLE D'OR (ADR-0006) : ce fichier porte le **schéma Zod commenté** (type +
|
|
11
|
+
* validation + défaut + doc) ET matérialise les défauts via `parse({})`. Aucune
|
|
12
|
+
* valeur n'est re-tapée ailleurs. Le builder (`defineModuleConfig.ts` →
|
|
13
|
+
* `defineDocumentationConfig`) importe le schéma D'ICI (nœud bas : ce fichier
|
|
14
|
+
* n'importe que `zod` → pas de cycle).
|
|
15
|
+
*
|
|
16
|
+
* La config est validée au boot du Module class (hook `onKernelRegister`, via
|
|
17
|
+
* le builder {@link defineDocumentationConfig}) → plante propre avec messages
|
|
18
|
+
* clairs si la config est invalide, plutôt qu'un `undefined.x` silencieux.
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ ENV : ce schéma reste PUR (pas de lecture `process.env` ici, sinon il
|
|
21
|
+
* deviendrait non déterministe et non sérialisable en JSON Schema pour Studio).
|
|
22
|
+
* La surcharge par variables d'environnement (`DOCS_REPO_URL`,
|
|
23
|
+
* `DOCS_REPO_BRANCH`) est appliquée dans {@link defineDocumentationConfig},
|
|
24
|
+
* APRÈS le parse.
|
|
25
|
+
*
|
|
26
|
+
* SURCHARGE PAR L'APPLICATION (fusion récursive) :
|
|
27
|
+
*
|
|
28
|
+
* // nodefony.config.ts
|
|
29
|
+
* use("@nodefony/documentation", {
|
|
30
|
+
* scan: { includeModules: false },
|
|
31
|
+
* repo: { url: "https://github.com/acme/app", editPathPrefix: "blob" },
|
|
32
|
+
* cache: { ttlMs: 0 },
|
|
33
|
+
* })
|
|
34
|
+
*
|
|
35
|
+
* ⚠️ NE PAS éditer les défauts matérialisés en bas de fichier : modifier les
|
|
36
|
+
* `.default(...)` du schéma. La validation + le merge env finaux sont faits dans
|
|
37
|
+
* `index.ts` au hook `onKernelRegister` via `defineDocumentationConfig`.
|
|
38
|
+
*/
|
|
39
|
+
const scanSchema = z.strictObject({
|
|
40
|
+
rootDir: z.string().min(1).default("docs").describe("Dossier de documentation transverse, relatif à la racine du projet (`kernel.path`). Défaut `docs`. Scanné récursivement pour les `.md`. C'est la doc qui n'appartient à aucun module (guides, ADR, audits)."),
|
|
41
|
+
includeModules: z.boolean().default(true).describe("Scanne aussi les `<module>/docs/*.md` co-localisés à chaque module chargé (ADR-0001 : la doc d'un module vit DANS le module). true = index transverse complet (racine + modules). false = racine seule (parité POC). La découverte des modules passe par `kernel.modules`."),
|
|
42
|
+
includeInstalled: z.boolean().default(true).describe("Scanne aussi la doc des paquets Nodefony INSTALLÉS mais pas encore chargés (`node_modules/@nodefony/*/docs` + le cœur `nodefony`). Sans cela, la doc d'un module non activé est introuvable — alors que c'est précisément le moment où on la lit : pour décider de l'activer. Les chemins sont résolus en real-path, donc un lien de workspace pointe vers la source, pas vers le lien symbolique."),
|
|
43
|
+
exclude: z.array(z.string().min(1)).default([
|
|
44
|
+
"session-retros",
|
|
45
|
+
"node_modules",
|
|
46
|
+
"dist"
|
|
47
|
+
]).describe("Noms de segments de chemin EXCLUS du scan (comparaison par segment, pas par préfixe). Défaut : retex de session, deps et build. Évite de surfacer du bruit ou des fichiers générés dans le portail.")
|
|
48
|
+
}).describe("Sources scannées pour construire l'index transverse de la doc.");
|
|
49
|
+
const repoSchema = z.strictObject({
|
|
50
|
+
url: z.string().min(1).default("https://github.com/nodefony/nodefony-core").describe("URL de base du dépôt (sans slash final), pour construire le lien « Modifier sur GitHub » d'une page. Surchargeable par l'env `DOCS_REPO_URL`. Aucun secret — URL publique uniquement."),
|
|
51
|
+
branch: z.string().min(1).optional().describe("Branche utilisée dans le lien d'édition. Si OMISE (défaut), la branche RÉELLE est résolue au runtime via `GitService.branch()` du core (lecture `.git/HEAD`, 0 spawn) → le lien suit toujours la branche courante. Surchargeable par l'env `DOCS_REPO_BRANCH` (utile en CI/prod détaché de git, ex. conteneur sans `.git`)."),
|
|
52
|
+
editPathPrefix: z.enum([
|
|
53
|
+
"edit",
|
|
54
|
+
"blob",
|
|
55
|
+
"tree"
|
|
56
|
+
]).default("edit").describe("Segment GitHub du lien source : `edit` (éditeur web), `blob` (lecture du fichier), `tree` (dossier). Défaut `edit`.")
|
|
57
|
+
}).describe("Identité du dépôt pour les liens d'édition des pages.");
|
|
58
|
+
const cacheSchema = z.strictObject({ ttlMs: z.number().int().nonnegative().default(3e4).describe("Durée de vie (ms) du cache de l'index (l'arbre des pages). Le scan FS n'est refait qu'à l'expiration. Défaut 30 s. 0 = pas de cache (chaque requête rescanne — pratique en dev pour voir un nouveau `.md` immédiatement). Le contenu d'une page n'est PAS caché (toujours relu).") }).describe("Politique de cache de l'index (chemin froid admin, lazy).");
|
|
59
|
+
const documentationConfigSchema = z.strictObject({
|
|
60
|
+
enabled: z.boolean().default(true).describe("Active le data plane de documentation au boot. false = module chargé mais inerte (endpoints inactifs) — utile pour couper la doc en prod si non désirée."),
|
|
61
|
+
scan: scanSchema.default(() => scanSchema.parse({})),
|
|
62
|
+
repo: repoSchema.default(() => repoSchema.parse({})),
|
|
63
|
+
cache: cacheSchema.default(() => cacheSchema.parse({}))
|
|
64
|
+
}).describe("Configuration de @nodefony/documentation.");
|
|
65
|
+
/**
|
|
66
|
+
* Défauts du module, matérialisés depuis le schéma (source unique). Toujours
|
|
67
|
+
* valides par construction ; passés au `super(..., config)` du Module class.
|
|
68
|
+
*/
|
|
69
|
+
const config = documentationConfigSchema.parse({});
|
|
70
|
+
//#endregion
|
|
71
|
+
export { config as default, documentationConfigSchema };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { documentationConfigSchema } from "./config.js";
|
|
2
|
+
import { parseModuleConfig } from "nodefony";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region nodefony/config/defineModuleConfig.ts
|
|
5
|
+
/**
|
|
6
|
+
* @nodefony/documentation — Builder de configuration validée (Zod) + ENV.
|
|
7
|
+
*
|
|
8
|
+
* ⭐ TL;DR : MACHINERIE DE BOOT — on n'édite (presque) jamais ce fichier. Même
|
|
9
|
+
* pattern que `nodefony.config.ts` ↔ `defineConfig()` du core : `config.ts` PORTE
|
|
10
|
+
* la config (schéma + défauts), `define<X>Config()` la VALIDE au boot (parse +
|
|
11
|
+
* env + freeze) et publie le JSON Schema Studio.
|
|
12
|
+
*
|
|
13
|
+
* Sépare la VALIDATION (schéma pur de `config.ts`) de l'APPLICATION des variables
|
|
14
|
+
* d'environnement : le schéma reste déterministe (sérialisable en JSON Schema
|
|
15
|
+
* pour Studio), et l'env est appliqué APRÈS le parse, ici.
|
|
16
|
+
*
|
|
17
|
+
* @see ./config.ts — source de vérité (types dérivés via z.infer)
|
|
18
|
+
*/
|
|
19
|
+
/** Lit une variable d'env non vide, ou `undefined` si absente/vide. */
|
|
20
|
+
function env(name) {
|
|
21
|
+
const v = process.env[name];
|
|
22
|
+
return v && v.trim() !== "" ? v.trim() : void 0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Valide une config partielle (défauts du schéma) PUIS applique la surcharge par
|
|
26
|
+
* variables d'environnement (précédence max).
|
|
27
|
+
*
|
|
28
|
+
* @param input - config partielle (depuis `module.options.documentation`)
|
|
29
|
+
* @returns config validée, défauts appliqués, env mergé
|
|
30
|
+
* @throws ZodError si l'input viole le schéma
|
|
31
|
+
*/
|
|
32
|
+
function defineDocumentationConfig(input = {}) {
|
|
33
|
+
const parsed = parseModuleConfig(documentationConfigSchema, input ?? {}, "@nodefony/documentation");
|
|
34
|
+
const repoUrlEnv = env("DOCS_REPO_URL");
|
|
35
|
+
if (repoUrlEnv) parsed.repo.url = repoUrlEnv;
|
|
36
|
+
const branchEnv = env("DOCS_REPO_BRANCH");
|
|
37
|
+
if (branchEnv) parsed.repo.branch = branchEnv;
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* JSON Schema introspectable de la config documentation — destiné au panneau de
|
|
42
|
+
* config Studio (`/nodefony/config`). N'inclut PAS la surcharge ENV (appliquée
|
|
43
|
+
* hors schéma, dans le builder).
|
|
44
|
+
*/
|
|
45
|
+
function documentationConfigJsonSchema() {
|
|
46
|
+
return z.toJSONSchema(documentationConfigSchema);
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { defineDocumentationConfig, documentationConfigJsonSchema, documentationConfigSchema };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { DocNotFoundError, DocUnsafeSlugError } from "../src/errors/DocumentationError.js";
|
|
2
|
+
import __decorateMetadata from "../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js";
|
|
3
|
+
import __decorate from "../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js";
|
|
4
|
+
import __decorateParam from "../../_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js";
|
|
5
|
+
import { Controller, Get, IsGranted, Param, Query, controller } from "@nodefony/framework";
|
|
6
|
+
import { Context } from "@nodefony/http";
|
|
7
|
+
//#region nodefony/controller/DocumentationController.ts
|
|
8
|
+
/**
|
|
9
|
+
* Data plane HTTP de la documentation Nodefony.
|
|
10
|
+
*
|
|
11
|
+
* Expose l'index transverse et le contenu des pages sous
|
|
12
|
+
* `/nodefony/documentation/api/*` (convention figée : un module admin sert
|
|
13
|
+
* toujours `/nodefony/<module>/api/*`, jamais une route mono-segment).
|
|
14
|
+
*
|
|
15
|
+
* **Mince par design** : toute la logique (scan, cache, allowlist, résolution
|
|
16
|
+
* `{{ }}`) vit dans `DocumentationService` (singleton stateful). Le controller
|
|
17
|
+
* est réinstancié par requête → il ne porte aucun état, il délègue.
|
|
18
|
+
*
|
|
19
|
+
* Sécurité (Zero Trust) : un slug invalide/inconnu renvoie un message
|
|
20
|
+
* **générique** au client ; le détail (slug, raison) est loggé côté serveur.
|
|
21
|
+
* La garde anti-traversée est dans le service ({@link isSafeSlug}).
|
|
22
|
+
*/
|
|
23
|
+
let DocumentationController = class DocumentationController extends Controller {
|
|
24
|
+
constructor(context) {
|
|
25
|
+
super("DocumentationController", context);
|
|
26
|
+
}
|
|
27
|
+
/** Résout le service de documentation depuis le container partagé. */
|
|
28
|
+
#service() {
|
|
29
|
+
const svc = this.get("documentation");
|
|
30
|
+
if (!svc) throw new Error("DocumentationService non enregistré");
|
|
31
|
+
return svc;
|
|
32
|
+
}
|
|
33
|
+
/** Index transverse : sections → pages, taguées par audience. */
|
|
34
|
+
async tree() {
|
|
35
|
+
try {
|
|
36
|
+
return this.renderJson(await this.#service().getTree());
|
|
37
|
+
} catch (e) {
|
|
38
|
+
this.log(e, "ERROR");
|
|
39
|
+
return this.renderJson({ error: "Index de documentation indisponible." }, 500);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Cherche dans le corpus — titres ET corps — et rend des extraits situés.
|
|
44
|
+
*
|
|
45
|
+
* Filtrer l'arbre ne répondait qu'à « quelle page s'appelle ainsi ? » ; la
|
|
46
|
+
* question posée est « où est-ce expliqué ? ».
|
|
47
|
+
*/
|
|
48
|
+
async search(q) {
|
|
49
|
+
try {
|
|
50
|
+
return this.renderJson(await this.#service().search(q ?? ""));
|
|
51
|
+
} catch (e) {
|
|
52
|
+
this.log(e, "ERROR");
|
|
53
|
+
return this.renderJson({ error: "Recherche indisponible." }, 500);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Contenu d'une page + variables `{{ }}` résolues côté serveur. */
|
|
57
|
+
async page(slug) {
|
|
58
|
+
try {
|
|
59
|
+
return this.renderJson(await this.#service().getPage(slug));
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (e instanceof DocNotFoundError || e instanceof DocUnsafeSlugError) {
|
|
62
|
+
this.log(`${e.docCode}: ${e.message}`, "WARNING");
|
|
63
|
+
return this.renderJson({
|
|
64
|
+
slug,
|
|
65
|
+
error: "Document inconnu."
|
|
66
|
+
}, 404);
|
|
67
|
+
}
|
|
68
|
+
this.log(e, "ERROR");
|
|
69
|
+
return this.renderJson({
|
|
70
|
+
slug,
|
|
71
|
+
error: "Lecture de la page impossible."
|
|
72
|
+
}, 500);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
__decorate([
|
|
77
|
+
IsGranted(["ROLE_DEV", "ROLE_SUPERVISOR"]),
|
|
78
|
+
Get("/documentation/api/tree"),
|
|
79
|
+
__decorateMetadata("design:type", Function),
|
|
80
|
+
__decorateMetadata("design:paramtypes", []),
|
|
81
|
+
__decorateMetadata("design:returntype", Promise)
|
|
82
|
+
], DocumentationController.prototype, "tree", null);
|
|
83
|
+
__decorate([
|
|
84
|
+
IsGranted(["ROLE_DEV", "ROLE_SUPERVISOR"]),
|
|
85
|
+
Get("/documentation/api/search"),
|
|
86
|
+
__decorateParam(0, Query("q")),
|
|
87
|
+
__decorateMetadata("design:type", Function),
|
|
88
|
+
__decorateMetadata("design:paramtypes", [String]),
|
|
89
|
+
__decorateMetadata("design:returntype", Promise)
|
|
90
|
+
], DocumentationController.prototype, "search", null);
|
|
91
|
+
__decorate([
|
|
92
|
+
IsGranted(["ROLE_DEV", "ROLE_SUPERVISOR"]),
|
|
93
|
+
Get("/documentation/api/page/{slug}"),
|
|
94
|
+
__decorateParam(0, Param("slug")),
|
|
95
|
+
__decorateMetadata("design:type", Function),
|
|
96
|
+
__decorateMetadata("design:paramtypes", [String]),
|
|
97
|
+
__decorateMetadata("design:returntype", Promise)
|
|
98
|
+
], DocumentationController.prototype, "page", null);
|
|
99
|
+
DocumentationController = __decorate([controller("/nodefony"), __decorateMetadata("design:paramtypes", [typeof Context === "undefined" ? Object : Context])], DocumentationController);
|
|
100
|
+
var DocumentationController_default = DocumentationController;
|
|
101
|
+
//#endregion
|
|
102
|
+
export { DocumentationController_default as default };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|