@komekoo/astro-cms 0.0.0-stage → 0.2.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 +144 -2
- package/dist/ajv.d.ts +4 -0
- package/dist/ajv.js +24 -0
- package/dist/apercu/arbre.d.ts +3 -0
- package/dist/apercu/arbre.js +64 -0
- package/dist/apercu/demarrage.d.ts +1 -0
- package/dist/apercu/demarrage.js +5 -0
- package/dist/apercu/index.d.ts +2 -0
- package/dist/apercu/index.js +2 -0
- package/dist/apercu/protocole.d.ts +67 -0
- package/dist/apercu/protocole.js +176 -0
- package/dist/apercu/script.d.ts +8 -0
- package/dist/apercu/script.js +321 -0
- package/dist/champs.d.ts +49 -0
- package/dist/champs.js +58 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +70 -0
- package/dist/collections.d.ts +39 -0
- package/dist/collections.js +57 -0
- package/dist/images.d.ts +8 -0
- package/dist/images.js +19 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +10 -0
- package/dist/integration.d.ts +9 -0
- package/dist/integration.js +113 -0
- package/dist/marqueur.d.ts +4 -0
- package/dist/marqueur.js +8 -0
- package/dist/motifs.d.ts +10 -0
- package/dist/motifs.js +11 -0
- package/dist/navigateur.d.ts +6 -0
- package/dist/navigateur.js +6 -0
- package/dist/noyau/apercu-build.d.ts +12 -0
- package/dist/noyau/apercu-build.js +74 -0
- package/dist/noyau/charger.d.ts +4 -0
- package/dist/noyau/charger.js +48 -0
- package/dist/noyau/collecter.d.ts +27 -0
- package/dist/noyau/collecter.js +133 -0
- package/dist/noyau/contenus.d.ts +6 -0
- package/dist/noyau/contenus.js +218 -0
- package/dist/noyau/erreurs.d.ts +7 -0
- package/dist/noyau/erreurs.js +14 -0
- package/dist/noyau/frontmatter.d.ts +15 -0
- package/dist/noyau/frontmatter.js +70 -0
- package/dist/noyau/json-schema.d.ts +4 -0
- package/dist/noyau/json-schema.js +61 -0
- package/dist/noyau/manifest.d.ts +16 -0
- package/dist/noyau/manifest.js +136 -0
- package/dist/noyau/markdown.d.ts +12 -0
- package/dist/noyau/markdown.js +95 -0
- package/dist/noyau/marqueurs.d.ts +11 -0
- package/dist/noyau/marqueurs.js +57 -0
- package/dist/noyau/operations.d.ts +12 -0
- package/dist/noyau/operations.js +54 -0
- package/dist/noyau/serialiser.d.ts +2 -0
- package/dist/noyau/serialiser.js +25 -0
- package/dist/noyau/version.d.ts +5 -0
- package/dist/noyau/version.js +25 -0
- package/dist/socle/hors-perimetre.d.ts +2 -0
- package/dist/socle/hors-perimetre.js +9 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.js +10 -0
- package/package.json +62 -3
- package/src/composants/CmsImage.astro +26 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { promises as fsp } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { chargerConfigAstro } from './noyau/charger.js';
|
|
5
|
+
import { formaterViolations } from './noyau/contenus.js';
|
|
6
|
+
import { MOTIF_SRC_IMAGE, MOTIF_URL_SITE } from './motifs.js';
|
|
7
|
+
import { ErreurCms } from './noyau/erreurs.js';
|
|
8
|
+
import { synchroniser, verifier } from './noyau/operations.js';
|
|
9
|
+
import { ecrireApercu } from './noyau/apercu-build.js';
|
|
10
|
+
import { verifierMarqueurs } from './noyau/marqueurs.js';
|
|
11
|
+
import { ecrireVersion } from './noyau/version.js';
|
|
12
|
+
const NOM = '@komekoo/astro-cms';
|
|
13
|
+
/** `Symbol.for` : la même clé que l'intégration soit chargée par Node ou par Vite (`chargerOptions`). */
|
|
14
|
+
export const OPTIONS = Symbol.for('@komekoo/astro-cms/options');
|
|
15
|
+
const estTexte = (v) => typeof v === 'string' && v.trim() !== '';
|
|
16
|
+
/** Options passées depuis `astro.config.mjs` (JavaScript, non typé) : vérifiées à l'appel de `cms()`. */
|
|
17
|
+
function verifierOptions(options) {
|
|
18
|
+
const o = (typeof options === 'object' && options !== null ? options : {});
|
|
19
|
+
for (const cle of ['site', 'contact']) {
|
|
20
|
+
if (!estTexte(o[cle]))
|
|
21
|
+
throw new ErreurCms(`${NOM} : option « ${cle} » obligatoire (texte non vide)`, 'astro.config.mjs');
|
|
22
|
+
}
|
|
23
|
+
const hp = o.horsPerimetre;
|
|
24
|
+
if (hp !== undefined && !(Array.isArray(hp) && hp.every((x) => typeof x === 'string'))) {
|
|
25
|
+
throw new ErreurCms(`${NOM} : option « horsPerimetre » doit être une liste de textes`, 'astro.config.mjs');
|
|
26
|
+
}
|
|
27
|
+
const refuser = (cle, attendu) => {
|
|
28
|
+
throw new ErreurCms(`${NOM} : option « ${cle} » ${attendu}`, 'astro.config.mjs');
|
|
29
|
+
};
|
|
30
|
+
if (!estTexte(o.url) || !MOTIF_URL_SITE.test(o.url) || o.url.endsWith('/')) {
|
|
31
|
+
refuser('url', 'obligatoire : adresse https sans « / » final (ex. https://exemple.fr)');
|
|
32
|
+
}
|
|
33
|
+
const admin = o.admin;
|
|
34
|
+
if (admin === undefined)
|
|
35
|
+
return;
|
|
36
|
+
if (typeof admin !== 'object' || admin === null || Array.isArray(admin)) {
|
|
37
|
+
return refuser('admin', 'doit être un objet { accent?, logo?, origine?, apercu? }');
|
|
38
|
+
}
|
|
39
|
+
const { accent, logo, origine, apercu } = admin;
|
|
40
|
+
if (accent !== undefined && !(typeof accent === 'string' && /^#[0-9A-Fa-f]{6}$/.test(accent))) {
|
|
41
|
+
refuser('admin.accent', 'doit être une couleur au format #RRGGBB');
|
|
42
|
+
}
|
|
43
|
+
if (logo !== undefined && !(typeof logo === 'string' && MOTIF_SRC_IMAGE.test(logo))) {
|
|
44
|
+
refuser('admin.logo', 'doit être une image téléversée (/uploads/…-2400.webp)');
|
|
45
|
+
}
|
|
46
|
+
if (origine !== undefined && !(typeof origine === 'string' && /^https:\/\/[^\s/]+$/.test(origine))) {
|
|
47
|
+
refuser('admin.origine', 'doit être une origine https sans chemin (ex. https://admin.exemple.fr)');
|
|
48
|
+
}
|
|
49
|
+
if (apercu !== undefined && !(typeof apercu === 'string' && MOTIF_URL_SITE.test(apercu) && !apercu.endsWith('/'))) {
|
|
50
|
+
refuser('admin.apercu', 'doit être une adresse https sans « / » final (ex. https://apercu.exemple.pages.dev)');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export default function cms(options) {
|
|
54
|
+
verifierOptions(options);
|
|
55
|
+
let racineProjet = '';
|
|
56
|
+
return {
|
|
57
|
+
name: NOM,
|
|
58
|
+
[OPTIONS]: options,
|
|
59
|
+
hooks: {
|
|
60
|
+
'astro:config:setup': async ({ config, command, addWatchFile, logger }) => {
|
|
61
|
+
const racine = fileURLToPath(config.root);
|
|
62
|
+
racineProjet = racine;
|
|
63
|
+
if (command === 'dev' || command === 'sync')
|
|
64
|
+
await synchroniser(racine, options);
|
|
65
|
+
if (command === 'dev')
|
|
66
|
+
addWatchFile(path.join(racine, 'src/content.config.ts'));
|
|
67
|
+
// Avant la synchronisation de contenu d'Astro (glob loader), qui s'arrête à la première erreur Zod.
|
|
68
|
+
if (command === 'build') {
|
|
69
|
+
const violations = await verifier(racine, options);
|
|
70
|
+
if (violations.length === 0)
|
|
71
|
+
return;
|
|
72
|
+
logger.error(formaterViolations(violations));
|
|
73
|
+
throw new ErreurCms(`${violations.length} problème(s) de contenu`);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
'astro:build:done': async ({ dir, logger }) => {
|
|
77
|
+
const dist = fileURLToPath(dir);
|
|
78
|
+
const schema = JSON.parse(await fsp.readFile(path.join(racineProjet, 'cms.schema.json'), 'utf8'));
|
|
79
|
+
const erreurs = await verifierMarqueurs(dist, schema.singletons ?? {});
|
|
80
|
+
if (erreurs.length > 0) {
|
|
81
|
+
for (const e of erreurs)
|
|
82
|
+
logger.error(`${e.page} — ${e.chemin} — ${e.motif}`);
|
|
83
|
+
throw new ErreurCms(`${erreurs.length} marqueur(s) d'aperçu invalide(s)`);
|
|
84
|
+
}
|
|
85
|
+
const origineAdmin = options.admin?.origine ?? 'https://cms.komekoo.link';
|
|
86
|
+
await ecrireVersion(dist, process.env.CF_PAGES_COMMIT_SHA ?? 'inconnu', origineAdmin);
|
|
87
|
+
await ecrireApercu(dist, {
|
|
88
|
+
origineAdmin,
|
|
89
|
+
branche: process.env.CF_PAGES_BRANCH,
|
|
90
|
+
avertir: (m) => logger.warn(m),
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function aplatir(valeur) {
|
|
97
|
+
return Array.isArray(valeur) ? valeur.flatMap(aplatir) : [valeur];
|
|
98
|
+
}
|
|
99
|
+
/** Retrouve les options passées à `cms()` dans `astro.config.mjs` (utilisé par la CLI). */
|
|
100
|
+
export async function chargerOptions(racine) {
|
|
101
|
+
const config = await chargerConfigAstro(racine);
|
|
102
|
+
const integrations = config?.integrations;
|
|
103
|
+
for (const integration of aplatir(integrations)) {
|
|
104
|
+
if (typeof integration !== 'object' || integration === null)
|
|
105
|
+
continue;
|
|
106
|
+
if (integration.name !== NOM)
|
|
107
|
+
continue;
|
|
108
|
+
const options = integration[OPTIONS];
|
|
109
|
+
if (options !== undefined)
|
|
110
|
+
return options;
|
|
111
|
+
}
|
|
112
|
+
throw new ErreurCms(`intégration ${NOM} absente de astro.config.mjs`);
|
|
113
|
+
}
|
package/dist/marqueur.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { MOTIF_CHEMIN_CMS } from './motifs.js';
|
|
2
|
+
/** Marqueur d'aperçu : `<h1 {...cms('accueil.titre')}>` pose `data-cms` pour que l'admin retrouve le champ. */
|
|
3
|
+
export function cms(chemin) {
|
|
4
|
+
if (!MOTIF_CHEMIN_CMS.test(chemin)) {
|
|
5
|
+
throw new Error(`chemin d'aperçu « ${chemin} » invalide : attendu ${MOTIF_CHEMIN_CMS.source}`);
|
|
6
|
+
}
|
|
7
|
+
return { 'data-cms': chemin };
|
|
8
|
+
}
|
package/dist/motifs.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const MOTIF_SRC_IMAGE: RegExp;
|
|
2
|
+
/**
|
|
3
|
+
* Schéma autorisé, puis aucun espace ni caractère de contrôle : les navigateurs suppriment tabulations et sauts
|
|
4
|
+
* de ligne (`/\t/evil.com` → `//evil.com`), d'où leur exclusion aussi juste après la barre oblique initiale.
|
|
5
|
+
*/
|
|
6
|
+
export declare const MOTIF_LIEN: RegExp;
|
|
7
|
+
/** Adresse publique du site : https, sans espace, sans « ? » ni « # » (le « / » final est refusé à part). */
|
|
8
|
+
export declare const MOTIF_URL_SITE: RegExp;
|
|
9
|
+
/** Chemin d'un champ dans une page : id du singleton, puis noms de champ ou indices (`reglages.horaires.2.ouverture`). */
|
|
10
|
+
export declare const MOTIF_CHEMIN_CMS: RegExp;
|
package/dist/motifs.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Motifs partagés entre le schéma (`champs.ts`, qui dépend d'Astro) et le navigateur : aucun import ici.
|
|
2
|
+
export const MOTIF_SRC_IMAGE = /^\/uploads\/(?:[a-z0-9-]+\/)*[a-z0-9-]+-2400\.webp$/;
|
|
3
|
+
/**
|
|
4
|
+
* Schéma autorisé, puis aucun espace ni caractère de contrôle : les navigateurs suppriment tabulations et sauts
|
|
5
|
+
* de ligne (`/\t/evil.com` → `//evil.com`), d'où leur exclusion aussi juste après la barre oblique initiale.
|
|
6
|
+
*/
|
|
7
|
+
export const MOTIF_LIEN = /^(https:\/\/[^\s\x00-\x1f]|\/(?![\/\\\s])|mailto:|tel:)[^\s\x00-\x1f]*$/;
|
|
8
|
+
/** Adresse publique du site : https, sans espace, sans « ? » ni « # » (le « / » final est refusé à part). */
|
|
9
|
+
export const MOTIF_URL_SITE = /^https:\/\/[^\s/?#]+(\/[^\s?#]*)?$/;
|
|
10
|
+
/** Chemin d'un champ dans une page : id du singleton, puis noms de champ ou indices (`reglages.horaires.2.ouverture`). */
|
|
11
|
+
export const MOTIF_CHEMIN_CMS = /^[a-z][a-z0-9-]*(\.([A-Za-z][A-Za-z0-9]*|\d+))+$/;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { creerAjv, MOTS_CLES_CMS } from './ajv.js';
|
|
2
|
+
export { validerMarkdown } from './noyau/markdown.js';
|
|
3
|
+
export type { Violation } from './noyau/markdown.js';
|
|
4
|
+
export { lireFrontmatter, ecrireFrontmatter } from './noyau/frontmatter.js';
|
|
5
|
+
export { MOTIF_CHEMIN_CMS, MOTIF_LIEN, MOTIF_SRC_IMAGE, MOTIF_URL_SITE } from './motifs.js';
|
|
6
|
+
export { srcsetDe, variantes } from './images.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Point d'entrée sûr pour le navigateur : n'importe rien de `node:*`, `astro` ni `vite`.
|
|
2
|
+
export { creerAjv, MOTS_CLES_CMS } from './ajv.js';
|
|
3
|
+
export { validerMarkdown } from './noyau/markdown.js';
|
|
4
|
+
export { lireFrontmatter, ecrireFrontmatter } from './noyau/frontmatter.js';
|
|
5
|
+
export { MOTIF_CHEMIN_CMS, MOTIF_LIEN, MOTIF_SRC_IMAGE, MOTIF_URL_SITE } from './motifs.js';
|
|
6
|
+
export { srcsetDe, variantes } from './images.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Contenu de `_cms/chargeur.js` : fichier externe (compatible avec une CSP sans `unsafe-inline`). */
|
|
2
|
+
export declare const CHARGEUR = "(function () {\n if (window.parent === window) return;\n if (!new URLSearchParams(location.search).has('cms-apercu')) return;\n var script = document.createElement('script');\n script.type = 'module';\n script.src = '/_cms/apercu/demarrage.js';\n document.head.appendChild(script);\n})();\n";
|
|
3
|
+
/**
|
|
4
|
+
* Ajoute au site construit le chargeur et le script d'aperçu (modules ES), la balise du chargeur dans
|
|
5
|
+
* chaque page HTML, et les en-têtes `_headers` qui autorisent l'affichage dans l'admin.
|
|
6
|
+
*/
|
|
7
|
+
export declare function ecrireApercu(dossierDist: string, o: {
|
|
8
|
+
origineAdmin: string;
|
|
9
|
+
branche?: string;
|
|
10
|
+
sources?: string;
|
|
11
|
+
avertir?: (message: string) => void;
|
|
12
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { promises as fsp } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
const PLACEHOLDER = "'__KOMEKOO_ORIGINE_ADMIN__'";
|
|
5
|
+
const BALISE = '<script src="/_cms/chargeur.js" async></script>';
|
|
6
|
+
const MARQUE_CSP = ' Content-Security-Policy: frame-ancestors';
|
|
7
|
+
const AVERTISSEMENT = "L'aperçu de l'admin ne pourra pas afficher ce site tant que X-Frame-Options est présent.";
|
|
8
|
+
/** Fichiers du script d'aperçu : chemin source (relatif à `dist/` du paquet) → chemin dans `<dist>/_cms/`. */
|
|
9
|
+
const MODULES = ['apercu/demarrage.js', 'apercu/script.js', 'apercu/protocole.js', 'motifs.js'];
|
|
10
|
+
/** Contenu de `_cms/chargeur.js` : fichier externe (compatible avec une CSP sans `unsafe-inline`). */
|
|
11
|
+
export const CHARGEUR = `(function () {
|
|
12
|
+
if (window.parent === window) return;
|
|
13
|
+
if (!new URLSearchParams(location.search).has('cms-apercu')) return;
|
|
14
|
+
var script = document.createElement('script');
|
|
15
|
+
script.type = 'module';
|
|
16
|
+
script.src = '/_cms/apercu/demarrage.js';
|
|
17
|
+
document.head.appendChild(script);
|
|
18
|
+
})();
|
|
19
|
+
`;
|
|
20
|
+
async function listerHtml(dossier) {
|
|
21
|
+
const entrees = await fsp.readdir(dossier, { withFileTypes: true });
|
|
22
|
+
const fichiers = await Promise.all(entrees.map(async (e) => {
|
|
23
|
+
const chemin = path.join(dossier, e.name);
|
|
24
|
+
if (e.isDirectory())
|
|
25
|
+
return listerHtml(chemin);
|
|
26
|
+
return e.name.endsWith('.html') ? [chemin] : [];
|
|
27
|
+
}));
|
|
28
|
+
return fichiers.flat();
|
|
29
|
+
}
|
|
30
|
+
async function injecterBalise(fichier) {
|
|
31
|
+
const html = await fsp.readFile(fichier, 'utf8');
|
|
32
|
+
if (html.includes(BALISE))
|
|
33
|
+
return;
|
|
34
|
+
const i = html.indexOf('</head>');
|
|
35
|
+
if (i === -1)
|
|
36
|
+
return;
|
|
37
|
+
await fsp.writeFile(fichier, html.slice(0, i) + BALISE + html.slice(i));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Ajoute au site construit le chargeur et le script d'aperçu (modules ES), la balise du chargeur dans
|
|
41
|
+
* chaque page HTML, et les en-têtes `_headers` qui autorisent l'affichage dans l'admin.
|
|
42
|
+
*/
|
|
43
|
+
export async function ecrireApercu(dossierDist, o) {
|
|
44
|
+
const sources = o.sources ?? fileURLToPath(new URL('../../dist/', import.meta.url));
|
|
45
|
+
await fsp.mkdir(path.join(dossierDist, '_cms/apercu'), { recursive: true });
|
|
46
|
+
await fsp.writeFile(path.join(dossierDist, '_cms/chargeur.js'), CHARGEUR);
|
|
47
|
+
for (const module of MODULES) {
|
|
48
|
+
let contenu = await fsp.readFile(path.join(sources, module), 'utf8');
|
|
49
|
+
if (module === 'apercu/demarrage.js')
|
|
50
|
+
contenu = contenu.replaceAll(PLACEHOLDER, JSON.stringify(o.origineAdmin));
|
|
51
|
+
await fsp.writeFile(path.join(dossierDist, '_cms', module), contenu);
|
|
52
|
+
}
|
|
53
|
+
for (const fichier of await listerHtml(dossierDist))
|
|
54
|
+
await injecterBalise(fichier);
|
|
55
|
+
const fichier = path.join(dossierDist, '_headers');
|
|
56
|
+
let existant = '';
|
|
57
|
+
try {
|
|
58
|
+
existant = await fsp.readFile(fichier, 'utf8');
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
if (e.code !== 'ENOENT')
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
if (o.avertir && existant.split('\n').some((l) => /x-frame-options:.*\b(deny|sameorigin)\b/i.test(l))) {
|
|
65
|
+
o.avertir(AVERTISSEMENT);
|
|
66
|
+
}
|
|
67
|
+
if (existant.split('\n').some((l) => l.startsWith(MARQUE_CSP)))
|
|
68
|
+
return;
|
|
69
|
+
let regle = `/*\n${MARQUE_CSP} 'self' ${o.origineAdmin}\n`;
|
|
70
|
+
if (o.branche !== undefined && o.branche !== 'main')
|
|
71
|
+
regle += ' X-Robots-Tag: noindex\n';
|
|
72
|
+
const separateur = existant === '' || existant.endsWith('\n') ? '' : '\n';
|
|
73
|
+
await fsp.writeFile(fichier, existant + separateur + regle);
|
|
74
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Charge `src/content.config.ts` du site et renvoie ses `collections`. */
|
|
2
|
+
export declare function chargerCollections(racine: string): Promise<Record<string, unknown>>;
|
|
3
|
+
/** Charge `astro.config.mjs` et renvoie son export par défaut. */
|
|
4
|
+
export declare function chargerConfigAstro(racine: string): Promise<unknown>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { createServer } from 'vite';
|
|
3
|
+
import { ErreurCms } from './erreurs.js';
|
|
4
|
+
const FICHIER_CONFIG = 'src/content.config.ts';
|
|
5
|
+
const FICHIER_ASTRO = 'astro.config.mjs';
|
|
6
|
+
/**
|
|
7
|
+
* Charge un module du site via Vite (même chaîne de résolution qu'Astro) et le passe à `lire`.
|
|
8
|
+
* Un serveur neuf par appel : `ssrLoadModule` met les modules en cache, une config modifiée doit être relue.
|
|
9
|
+
*/
|
|
10
|
+
async function chargerModule(racine, fichier, lire) {
|
|
11
|
+
const serveur = await createServer({
|
|
12
|
+
root: path.resolve(racine),
|
|
13
|
+
appType: 'custom',
|
|
14
|
+
server: { middlewareMode: true, watch: null, ws: false },
|
|
15
|
+
optimizeDeps: { noDiscovery: true },
|
|
16
|
+
logLevel: 'silent',
|
|
17
|
+
configFile: false,
|
|
18
|
+
});
|
|
19
|
+
try {
|
|
20
|
+
return lire(await serveur.ssrLoadModule(`/${fichier}`));
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
if (e instanceof ErreurCms)
|
|
24
|
+
throw e;
|
|
25
|
+
const cause = e instanceof Error ? e.message : String(e);
|
|
26
|
+
if (cause.includes('astro:content')) {
|
|
27
|
+
throw new ErreurCms("import de « astro:content » non pris en charge par komekoo-cms : utilise astro/zod, astro/loaders et l'API de @komekoo/astro-cms", fichier);
|
|
28
|
+
}
|
|
29
|
+
throw new ErreurCms(`chargement impossible : ${cause}`, fichier);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
await serveur.close();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Charge `src/content.config.ts` du site et renvoie ses `collections`. */
|
|
36
|
+
export function chargerCollections(racine) {
|
|
37
|
+
return chargerModule(racine, FICHIER_CONFIG, (module) => {
|
|
38
|
+
const collections = module['collections'];
|
|
39
|
+
if (typeof collections !== 'object' || collections === null) {
|
|
40
|
+
throw new ErreurCms('`collections` n’est pas exporté', FICHIER_CONFIG);
|
|
41
|
+
}
|
|
42
|
+
return collections;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Charge `astro.config.mjs` et renvoie son export par défaut. */
|
|
46
|
+
export function chargerConfigAstro(racine) {
|
|
47
|
+
return chargerModule(racine, FICHIER_ASTRO, (module) => module['default']);
|
|
48
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { z } from 'astro/zod';
|
|
2
|
+
import type { Editeur } from '../types.js';
|
|
3
|
+
export interface ModeleSingleton {
|
|
4
|
+
id: string;
|
|
5
|
+
libelle: string;
|
|
6
|
+
/** `src/content/pages/<id>.json` */
|
|
7
|
+
fichier: string;
|
|
8
|
+
adresse?: string;
|
|
9
|
+
apercu?: string;
|
|
10
|
+
schema: z.ZodObject;
|
|
11
|
+
}
|
|
12
|
+
export interface ModeleCollection {
|
|
13
|
+
id: string;
|
|
14
|
+
libelle: string;
|
|
15
|
+
dossier: string;
|
|
16
|
+
format: 'json' | 'markdown';
|
|
17
|
+
titre: string;
|
|
18
|
+
ordonnable: boolean;
|
|
19
|
+
schema: z.ZodObject;
|
|
20
|
+
}
|
|
21
|
+
export interface ModeleCms {
|
|
22
|
+
singletons: ModeleSingleton[];
|
|
23
|
+
collections: ModeleCollection[];
|
|
24
|
+
}
|
|
25
|
+
export declare function collecter(collections: Record<string, unknown>): ModeleCms;
|
|
26
|
+
/** Déduit l'éditeur d'un schéma Zod brut ; la méta CMS prime quand elle existe. */
|
|
27
|
+
export declare function deduireEditeur(schema: z.ZodType): Editeur;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { lireDecl } from '../collections.js';
|
|
2
|
+
import { lireMeta } from '../types.js';
|
|
3
|
+
import { ErreurCms } from './erreurs.js';
|
|
4
|
+
const def = (s) => s._zod.def;
|
|
5
|
+
const REFUSES = {
|
|
6
|
+
function: 'type « function » non sérialisable',
|
|
7
|
+
transform: 'transformation non sérialisable',
|
|
8
|
+
any: 'type « any » refusé : déclare un type précis',
|
|
9
|
+
unknown: 'type « unknown » refusé : déclare un type précis',
|
|
10
|
+
date: 'type « date » refusé : utilise champ.date()',
|
|
11
|
+
};
|
|
12
|
+
const FEUILLES = new Set(['string', 'number', 'boolean', 'enum', 'literal', 'null']);
|
|
13
|
+
const ENVELOPPES = new Set(['optional', 'nullable', 'default', 'prefault', 'nonoptional', 'readonly']);
|
|
14
|
+
function verifier(schema, chemin, vus) {
|
|
15
|
+
if (vus.has(schema))
|
|
16
|
+
return;
|
|
17
|
+
vus.add(schema);
|
|
18
|
+
const d = def(schema);
|
|
19
|
+
const refus = REFUSES[d.type];
|
|
20
|
+
if (refus !== undefined)
|
|
21
|
+
throw new ErreurCms(`${chemin} : ${refus}`);
|
|
22
|
+
if (FEUILLES.has(d.type))
|
|
23
|
+
return;
|
|
24
|
+
if (ENVELOPPES.has(d.type)) {
|
|
25
|
+
if (d.innerType)
|
|
26
|
+
verifier(d.innerType, chemin, vus);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
switch (d.type) {
|
|
30
|
+
case 'object':
|
|
31
|
+
for (const [cle, sous] of Object.entries(d.shape ?? {}))
|
|
32
|
+
verifier(sous, `${chemin}.${cle}`, vus);
|
|
33
|
+
return;
|
|
34
|
+
case 'array':
|
|
35
|
+
if (d.element)
|
|
36
|
+
verifier(d.element, `${chemin}[]`, vus);
|
|
37
|
+
return;
|
|
38
|
+
case 'union':
|
|
39
|
+
if (d.discriminator === undefined) {
|
|
40
|
+
throw new ErreurCms(`${chemin} : union sans discriminant refusée : utilise z.discriminatedUnion()`);
|
|
41
|
+
}
|
|
42
|
+
for (const option of d.options ?? [])
|
|
43
|
+
verifier(option, chemin, vus);
|
|
44
|
+
return;
|
|
45
|
+
case 'pipe':
|
|
46
|
+
if (def(d.in).type === 'transform' || def(d.out).type === 'transform') {
|
|
47
|
+
throw new ErreurCms(`${chemin} : ${REFUSES.transform}`);
|
|
48
|
+
}
|
|
49
|
+
verifier(d.in, chemin, vus);
|
|
50
|
+
verifier(d.out, chemin, vus);
|
|
51
|
+
return;
|
|
52
|
+
default:
|
|
53
|
+
throw new ErreurCms(`${chemin} : type « ${d.type} » non pris en charge`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function collecter(collections) {
|
|
57
|
+
const modele = { singletons: [], collections: [] };
|
|
58
|
+
for (const [cle, valeur] of Object.entries(collections)) {
|
|
59
|
+
const decl = lireDecl(valeur);
|
|
60
|
+
if (!decl)
|
|
61
|
+
continue;
|
|
62
|
+
if (cle !== decl.id) {
|
|
63
|
+
throw new ErreurCms(`la clé « ${cle} » de l'export collections diffère de l'id déclaré « ${decl.id} »`);
|
|
64
|
+
}
|
|
65
|
+
const schema = valeur.schema;
|
|
66
|
+
verifier(schema, decl.id, new WeakSet());
|
|
67
|
+
if (decl.type === 'singleton') {
|
|
68
|
+
modele.singletons.push({
|
|
69
|
+
id: decl.id,
|
|
70
|
+
libelle: decl.libelle,
|
|
71
|
+
fichier: `src/content/pages/${decl.id}.json`,
|
|
72
|
+
...(decl.adresse !== undefined && { adresse: decl.adresse }),
|
|
73
|
+
...(decl.apercu !== undefined && { apercu: decl.apercu }),
|
|
74
|
+
schema,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
modele.collections.push({
|
|
79
|
+
id: decl.id,
|
|
80
|
+
libelle: decl.libelle,
|
|
81
|
+
dossier: `src/content/${decl.id}`,
|
|
82
|
+
format: decl.format,
|
|
83
|
+
titre: decl.titre,
|
|
84
|
+
ordonnable: decl.ordonnable,
|
|
85
|
+
schema,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const parId = (a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
90
|
+
modele.singletons.sort(parId);
|
|
91
|
+
modele.collections.sort(parId);
|
|
92
|
+
return modele;
|
|
93
|
+
}
|
|
94
|
+
/** Déduit l'éditeur d'un schéma Zod brut ; la méta CMS prime quand elle existe. */
|
|
95
|
+
export function deduireEditeur(schema) {
|
|
96
|
+
const meta = lireMeta(schema);
|
|
97
|
+
if (meta)
|
|
98
|
+
return meta.editeur;
|
|
99
|
+
const d = def(schema);
|
|
100
|
+
switch (d.type) {
|
|
101
|
+
case 'string':
|
|
102
|
+
return 'texte';
|
|
103
|
+
case 'literal': {
|
|
104
|
+
const t = typeof d.values?.[0];
|
|
105
|
+
if (t === 'string')
|
|
106
|
+
return 'texte';
|
|
107
|
+
if (t === 'number')
|
|
108
|
+
return 'nombre';
|
|
109
|
+
if (t === 'boolean')
|
|
110
|
+
return 'booleen';
|
|
111
|
+
throw new ErreurCms('impossible de déduire un éditeur pour ce littéral');
|
|
112
|
+
}
|
|
113
|
+
case 'number':
|
|
114
|
+
return 'nombre';
|
|
115
|
+
case 'boolean':
|
|
116
|
+
return 'booleen';
|
|
117
|
+
case 'enum':
|
|
118
|
+
return 'choix';
|
|
119
|
+
case 'array':
|
|
120
|
+
return 'liste';
|
|
121
|
+
case 'object':
|
|
122
|
+
return 'groupe';
|
|
123
|
+
case 'optional':
|
|
124
|
+
case 'nullable':
|
|
125
|
+
case 'default':
|
|
126
|
+
case 'prefault':
|
|
127
|
+
case 'nonoptional':
|
|
128
|
+
case 'readonly':
|
|
129
|
+
return deduireEditeur(d.innerType);
|
|
130
|
+
default:
|
|
131
|
+
throw new ErreurCms(`impossible de déduire un éditeur pour le type « ${d.type} »`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Violation } from './markdown.js';
|
|
2
|
+
import type { ModeleCms } from './collecter.js';
|
|
3
|
+
/** Valide tous les fichiers de contenu d'un site. Ordre : singletons par id, collections par id, fichiers triés. */
|
|
4
|
+
export declare function validerContenus(racine: string, modele: ModeleCms): Promise<Violation[]>;
|
|
5
|
+
/** Une ligne par violation : `fichier:ligne — motif` (ordre d'entrée conservé). */
|
|
6
|
+
export declare function formaterViolations(v: Violation[]): string;
|