@komekoo/astro-cms 0.0.0-stage → 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 (47) hide show
  1. package/README.md +127 -2
  2. package/dist/ajv.d.ts +4 -0
  3. package/dist/ajv.js +23 -0
  4. package/dist/champs.d.ts +49 -0
  5. package/dist/champs.js +58 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.js +70 -0
  8. package/dist/collections.d.ts +38 -0
  9. package/dist/collections.js +52 -0
  10. package/dist/images.d.ts +8 -0
  11. package/dist/images.js +19 -0
  12. package/dist/index.d.ts +12 -0
  13. package/dist/index.js +9 -0
  14. package/dist/integration.d.ts +9 -0
  15. package/dist/integration.js +91 -0
  16. package/dist/motifs.d.ts +8 -0
  17. package/dist/motifs.js +9 -0
  18. package/dist/navigateur.d.ts +5 -0
  19. package/dist/navigateur.js +5 -0
  20. package/dist/noyau/charger.d.ts +4 -0
  21. package/dist/noyau/charger.js +48 -0
  22. package/dist/noyau/collecter.d.ts +26 -0
  23. package/dist/noyau/collecter.js +132 -0
  24. package/dist/noyau/contenus.d.ts +6 -0
  25. package/dist/noyau/contenus.js +218 -0
  26. package/dist/noyau/erreurs.d.ts +7 -0
  27. package/dist/noyau/erreurs.js +14 -0
  28. package/dist/noyau/frontmatter.d.ts +15 -0
  29. package/dist/noyau/frontmatter.js +70 -0
  30. package/dist/noyau/json-schema.d.ts +4 -0
  31. package/dist/noyau/json-schema.js +58 -0
  32. package/dist/noyau/manifest.d.ts +15 -0
  33. package/dist/noyau/manifest.js +136 -0
  34. package/dist/noyau/markdown.d.ts +11 -0
  35. package/dist/noyau/markdown.js +95 -0
  36. package/dist/noyau/operations.d.ts +12 -0
  37. package/dist/noyau/operations.js +54 -0
  38. package/dist/noyau/serialiser.d.ts +2 -0
  39. package/dist/noyau/serialiser.js +25 -0
  40. package/dist/noyau/version.d.ts +5 -0
  41. package/dist/noyau/version.js +25 -0
  42. package/dist/socle/hors-perimetre.d.ts +2 -0
  43. package/dist/socle/hors-perimetre.js +9 -0
  44. package/dist/types.d.ts +33 -0
  45. package/dist/types.js +10 -0
  46. package/package.json +53 -3
  47. package/src/composants/CmsImage.astro +24 -0
package/README.md CHANGED
@@ -1,3 +1,128 @@
1
- # Temporary Holding Version
1
+ # @komekoo/astro-cms
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ CMS git pour sites Astro : les contenus sont des fichiers JSON et Markdown dans `src/content/`, décrits par des
4
+ champs typés (`champ.*`). Le paquet génère le schéma CMS (`cms.schema.json`) et le manifeste destiné aux agents IA
5
+ (`ai/manifest.json`), et vérifie les contenus avant chaque `astro build`.
6
+
7
+ ## Installation
8
+
9
+ Prérequis : Node ≥ 22.23.4, Astro ^7.4.2.
10
+
11
+ ```sh
12
+ pnpm add @komekoo/astro-cms
13
+ ```
14
+
15
+ ## Déclarer les contenus : `src/content.config.ts`
16
+
17
+ ```ts
18
+ // N'importe pas `astro:content` ici : komekoo-cms charge ce fichier hors d'Astro (sync / check).
19
+ import { champ, defineCollection, defineSingleton } from '@komekoo/astro-cms';
20
+
21
+ // Singleton : un seul fichier, src/content/pages/accueil.json
22
+ const accueil = defineSingleton('accueil', {
23
+ libelle: "Page d'accueil",
24
+ schema: {
25
+ titre: champ.texte({ libelle: 'Titre principal', max: 80 }),
26
+ image: champ.image({ libelle: 'Image principale' }),
27
+ presentation: champ.richtext({ libelle: 'Présentation' }),
28
+ reservation: champ.lien({ libelle: 'Lien de réservation', obligatoire: false }),
29
+ },
30
+ });
31
+
32
+ // Collection : un fichier par élément, dans src/content/tarifs/
33
+ const tarifs = defineCollection('tarifs', {
34
+ libelle: 'Tarifs',
35
+ format: 'json', // ou 'markdown' (frontmatter YAML + corps)
36
+ titre: 'prestation', // champ texte servant de titre dans l'admin
37
+ ordonnable: true, // ajoute un champ `ordre` (entier ≥ 0)
38
+ schema: {
39
+ prestation: champ.texte({ libelle: 'Prestation', max: 80 }),
40
+ categorie: champ.choix({ libelle: 'Catégorie', options: ['Femme', 'Homme', 'Enfant'] }),
41
+ prix: champ.nombre({ libelle: 'Prix', unite: '€', min: 0 }),
42
+ },
43
+ });
44
+
45
+ export const collections = { accueil, tarifs };
46
+ ```
47
+
48
+ Champs disponibles : `texte`, `paragraphe`, `richtext`, `image`, `date`, `lien`, `nombre`, `booleen`, `choix`,
49
+ `liste`, `groupe`. Chacun prend `libelle`, `aide` (facultatif) et `obligatoire` (`true` par défaut). Les types
50
+ TypeScript suivent la déclaration : un champ `obligatoire: false` est typé `T | undefined`, `choix` donne l'union
51
+ de ses options.
52
+
53
+ - `onglet` (facultatif, texte non vide) : range le champ dans un onglet du formulaire d'administration, par ex.
54
+ `onglet: 'Référencement Google'`.
55
+ - `adresse` (option de `defineSingleton`, facultative) : adresse publique de la page, par ex. `adresse: '/a-propos'`
56
+ (`/` suivi de minuscules, chiffres, `-` ou `/`). Elle permet à l'admin d'ouvrir l'aperçu de la page.
57
+ - Images : `src` de la forme `/uploads/…/nom-2400.webp`, avec les variantes `-800.webp`, `-1600.webp` et
58
+ `-2400.webp` présentes dans `public/uploads/`.
59
+ - Liens : `https://…`, chemin interne `/…`, `mailto:` ou `tel:`.
60
+ - Markdown (`richtext` et corps des articles) : titres 2 et 3, gras, italique, liens, listes, citations, images
61
+ `/uploads/`, tableaux GFM simples. HTML, code, barré, cases à cocher, notes et lignes horizontales sont refusés.
62
+
63
+ ## Intégration : `astro.config.mjs`
64
+
65
+ ```js
66
+ import { defineConfig } from 'astro/config';
67
+ import cms from '@komekoo/astro-cms/integration';
68
+
69
+ export default defineConfig({
70
+ integrations: [
71
+ cms({
72
+ site: 'Salon de coiffure à Vichy', // présentation du site dans ai/manifest.json (obligatoire)
73
+ contact: 'contact@exemple.fr', // contact proposé pour les demandes hors périmètre (obligatoire)
74
+ url: 'https://exemple.fr', // adresse publique du site, https sans « / » final (obligatoire)
75
+ admin: { accent: '#5DEFB7', logo: '/uploads/logo-2400.webp', origine: 'https://admin.exemple.fr' }, // facultatif
76
+ horsPerimetre: ['Modifier le menu de navigation'], // ajouts à la liste socle (facultatif)
77
+ }),
78
+ ],
79
+ });
80
+ ```
81
+
82
+ - `astro dev` et `astro sync` régénèrent `cms.schema.json` et `ai/manifest.json`.
83
+ - `astro build` vérifie les fichiers générés et tous les contenus, puis échoue avec une ligne
84
+ `fichier:ligne — motif` par problème.
85
+
86
+ ## CLI
87
+
88
+ ```sh
89
+ komekoo-cms sync # génère cms.schema.json et ai/manifest.json
90
+ komekoo-cms check # vérifie les fichiers générés et valide les contenus (code 1 si problème)
91
+ ```
92
+
93
+ ## Point d'entrée navigateur : `@komekoo/astro-cms/navigateur`
94
+
95
+ Pour les outils qui tournent dans un navigateur (ex. l'interface d'administration) : n'importe rien de `node:*`,
96
+ `astro` ni `vite`. Exporte `creerAjv` et `MOTS_CLES_CMS` (validation du schéma CMS), `validerMarkdown` (et le
97
+ type `Violation`), `lireFrontmatter`, `ecrireFrontmatter` (écrit `---`, YAML, `---`, corps ; dates entre
98
+ guillemets), `MOTIF_LIEN` et `MOTIF_SRC_IMAGE`.
99
+
100
+ Options récentes : `url` (obligatoire) et `admin` de `cms()`, `onglet` sur les champs, `adresse` sur les
101
+ singletons, et le bloc `x-cms-site` de `cms.schema.json`.
102
+
103
+ ## Composant `<CmsImage>`
104
+
105
+ ```astro
106
+ ---
107
+ import CmsImage from '@komekoo/astro-cms/CmsImage.astro';
108
+ ---
109
+ <CmsImage image={page.image} sizes="(min-width: 60rem) 50vw, 100vw" loading="eager" class="heros" />
110
+ ```
111
+
112
+ Produit un `<img>` avec `srcset` sur les variantes 800 / 1600 / 2400, `width`, `height` et `alt`.
113
+ Props : `image` (obligatoire), `sizes` (`100vw` par défaut), `loading` (`lazy` par défaut), `class`.
114
+
115
+ ## Limites connues
116
+
117
+ - `src/content.config.ts` ne doit pas importer `astro:content` : il est chargé hors d'Astro par la CLI.
118
+ - Frontmatter TOML (`+++`) refusé : utiliser YAML (`---`). Mettre les dates entre guillemets
119
+ (`date: "2026-10-01"`) ; une date nue est lue comme un horodatage YAML et refusée.
120
+ - Node ≥ 22.23.4 (exigence d'Astro 7.4.2).
121
+ - Les fichiers générés (`cms.schema.json`, `ai/manifest.json`) doivent être committés ; un fichier périmé fait
122
+ échouer `astro build` et `komekoo-cms check`. Pour éviter les conversions CRLF sous Windows, ajouter à
123
+ `.gitattributes` :
124
+
125
+ ```gitattributes
126
+ **/cms.schema.json text eol=lf
127
+ **/ai/manifest.json text eol=lf
128
+ ```
package/dist/ajv.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { Ajv2020 } from 'ajv/dist/2020.js';
2
+ export declare const MOTS_CLES_CMS: readonly string[];
3
+ /** Ajv 2020-12 strict, formats activés, mots-clés `x-cms*` déclarés. */
4
+ export declare function creerAjv(): Ajv2020;
package/dist/ajv.js ADDED
@@ -0,0 +1,23 @@
1
+ import { Ajv2020 } from 'ajv/dist/2020.js';
2
+ import addFormats from 'ajv-formats';
3
+ export const MOTS_CLES_CMS = [
4
+ 'x-cms',
5
+ 'x-cms-version',
6
+ 'x-cms-site',
7
+ 'x-cms-libelle',
8
+ 'x-cms-fichier',
9
+ 'x-cms-adresse',
10
+ 'x-cms-dossier',
11
+ 'x-cms-format',
12
+ 'x-cms-titre',
13
+ 'x-cms-corps',
14
+ 'x-cms-ordonnable',
15
+ ];
16
+ /** Ajv 2020-12 strict, formats activés, mots-clés `x-cms*` déclarés. */
17
+ export function creerAjv() {
18
+ const ajv = new Ajv2020({ strict: true, allErrors: true });
19
+ addFormats(ajv);
20
+ for (const mot of MOTS_CLES_CMS)
21
+ ajv.addKeyword(mot);
22
+ return ajv;
23
+ }
@@ -0,0 +1,49 @@
1
+ import { z } from 'astro/zod';
2
+ import type { OptionsBase } from './types.js';
3
+ import { MOTIF_LIEN, MOTIF_SRC_IMAGE } from './motifs.js';
4
+ export { MOTIF_LIEN, MOTIF_SRC_IMAGE };
5
+ type Bornes = {
6
+ min?: number;
7
+ max?: number;
8
+ };
9
+ /** `obligatoire: false` → `ZodOptional<T>` ; absent ou `true` → `T` ; `boolean` non littéral → l'un ou l'autre. */
10
+ export type Finalise<T extends z.ZodType, O extends OptionsBase> = O extends {
11
+ obligatoire: false;
12
+ } ? z.ZodOptional<T> : O extends {
13
+ obligatoire: true;
14
+ } ? T : 'obligatoire' extends keyof O ? T | z.ZodOptional<T> : T;
15
+ export declare const champ: {
16
+ texte: <O extends OptionsBase & Bornes>(o: O) => Finalise<z.ZodString, O>;
17
+ paragraphe: <O extends OptionsBase & Bornes>(o: O) => Finalise<z.ZodString, O>;
18
+ richtext: <O extends OptionsBase & {
19
+ max?: number;
20
+ }>(o: O) => Finalise<z.ZodString, O>;
21
+ image: <O extends OptionsBase>(o: O) => Finalise<z.ZodObject<{
22
+ src: z.ZodString;
23
+ alt: z.ZodString;
24
+ largeur: z.ZodInt;
25
+ hauteur: z.ZodInt;
26
+ }, z.core.$strict>, O>;
27
+ date: <O extends OptionsBase>(o: O) => Finalise<z.ZodISODate, O>;
28
+ lien: <O extends OptionsBase>(o: O) => Finalise<z.ZodString, O>;
29
+ nombre: <O extends OptionsBase & Bornes & {
30
+ entier?: boolean;
31
+ unite?: string;
32
+ }>(o: O) => Finalise<z.ZodNumber, O>;
33
+ booleen: <O extends OptionsBase>(o: O) => Finalise<z.ZodBoolean, O>;
34
+ choix: <const Options extends readonly [string, ...string[]], O extends OptionsBase>(o: O & {
35
+ options: Options;
36
+ }) => Finalise<z.ZodEnum<{ [k in Options[number]]: k; } extends infer T ? { [k in keyof T]: T[k]; } : never>, O & {
37
+ options: Options;
38
+ }>;
39
+ liste: <E extends z.ZodType, O extends OptionsBase & Bornes>(o: O & {
40
+ element: E;
41
+ }) => Finalise<z.ZodArray<E>, O & {
42
+ element: E;
43
+ }>;
44
+ groupe: <S extends Record<string, z.ZodType>, O extends OptionsBase>(o: O & {
45
+ champs: S;
46
+ }) => Finalise<z.ZodObject<{ -readonly [P in keyof S]: S[P]; }, z.core.$strict>, O & {
47
+ champs: S;
48
+ }>;
49
+ };
package/dist/champs.js ADDED
@@ -0,0 +1,58 @@
1
+ import { z } from 'astro/zod';
2
+ import { MOTIF_LIEN, MOTIF_SRC_IMAGE } from './motifs.js';
3
+ export { MOTIF_LIEN, MOTIF_SRC_IMAGE };
4
+ /** Appose la méta CMS puis, si le champ n'est pas obligatoire, `.optional()`. */
5
+ function finaliser(schema, o, editeur, extra = {}) {
6
+ const cms = { editeur, libelle: o.libelle };
7
+ if (o.aide !== undefined)
8
+ cms.aide = o.aide;
9
+ if (o.onglet !== undefined) {
10
+ if (o.onglet === '')
11
+ throw new Error(`onglet du champ « ${o.libelle} » : texte non vide attendu`);
12
+ cms.onglet = o.onglet;
13
+ }
14
+ if (extra.unite !== undefined)
15
+ cms.unite = extra.unite;
16
+ const avecMeta = schema.meta({ cms });
17
+ return (o.obligatoire === false ? avecMeta.optional() : avecMeta);
18
+ }
19
+ function chaine({ min, max }) {
20
+ let s = z.string();
21
+ if (min !== undefined)
22
+ s = s.min(min);
23
+ if (max !== undefined)
24
+ s = s.max(max);
25
+ return s;
26
+ }
27
+ export const champ = {
28
+ texte: (o) => finaliser(chaine(o), o, 'texte'),
29
+ paragraphe: (o) => finaliser(chaine(o), o, 'paragraphe'),
30
+ richtext: (o) => finaliser(chaine({ max: o.max }), o, 'richtext'),
31
+ image: (o) => finaliser(z.strictObject({
32
+ src: z.string().regex(MOTIF_SRC_IMAGE),
33
+ alt: z.string().max(250),
34
+ largeur: z.int().positive(),
35
+ hauteur: z.int().positive(),
36
+ }), o, 'image'),
37
+ date: (o) => finaliser(z.iso.date(), o, 'date'),
38
+ lien: (o) => finaliser(z.string().regex(MOTIF_LIEN), o, 'lien'),
39
+ nombre: (o) => {
40
+ let s = o.entier ? z.int() : z.number();
41
+ if (o.min !== undefined)
42
+ s = s.min(o.min);
43
+ if (o.max !== undefined)
44
+ s = s.max(o.max);
45
+ return finaliser(s, o, 'nombre', { unite: o.unite });
46
+ },
47
+ booleen: (o) => finaliser(z.boolean(), o, 'booleen'),
48
+ choix: (o) => finaliser(z.enum(o.options), o, 'choix'),
49
+ liste: (o) => {
50
+ let s = z.array(o.element);
51
+ if (o.min !== undefined)
52
+ s = s.min(o.min);
53
+ if (o.max !== undefined)
54
+ s = s.max(o.max);
55
+ return finaliser(s, o, 'liste');
56
+ },
57
+ groupe: (o) => finaliser(z.strictObject(o.champs), o, 'groupe'),
58
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ /** Cœur testable de la CLI : renvoie le code de sortie. */
3
+ export declare function executerCli(argv: string[], racine: string): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { parseArgs } from 'node:util';
5
+ import { chargerOptions } from './integration.js';
6
+ import { formaterViolations } from './noyau/contenus.js';
7
+ import { ErreurCms } from './noyau/erreurs.js';
8
+ import { synchroniser, verifier } from './noyau/operations.js';
9
+ const AIDE = `Usage : komekoo-cms <commande>
10
+
11
+ Commandes :
12
+ sync génère cms.schema.json et ai/manifest.json
13
+ check vérifie les fichiers générés et valide les contenus (code 1 si problème)
14
+
15
+ Options :
16
+ -h, --help affiche cette aide`;
17
+ /** Cœur testable de la CLI : renvoie le code de sortie. */
18
+ export async function executerCli(argv, racine) {
19
+ const { positionals, values } = parseArgs({
20
+ args: argv,
21
+ allowPositionals: true,
22
+ options: { help: { type: 'boolean', short: 'h' } },
23
+ });
24
+ const commande = positionals[0];
25
+ if (values.help || (commande !== 'sync' && commande !== 'check')) {
26
+ console.error(AIDE);
27
+ return values.help && commande === undefined ? 0 : 2;
28
+ }
29
+ try {
30
+ const options = await chargerOptions(racine);
31
+ if (commande === 'sync') {
32
+ const ecrits = await synchroniser(racine, options);
33
+ console.log(ecrits.length === 0 ? 'Fichiers à jour.' : ecrits.map((f) => `réécrit : ${f}`).join('\n'));
34
+ return 0;
35
+ }
36
+ const violations = await verifier(racine, options);
37
+ if (violations.length === 0) {
38
+ console.log('Aucun problème.');
39
+ return 0;
40
+ }
41
+ console.log(formaterViolations(violations));
42
+ return 1;
43
+ }
44
+ catch (e) {
45
+ if (!(e instanceof ErreurCms))
46
+ throw e;
47
+ console.error(e.message);
48
+ return 1;
49
+ }
50
+ }
51
+ function estPrincipal() {
52
+ const script = process.argv[1];
53
+ if (script === undefined)
54
+ return false;
55
+ try {
56
+ return import.meta.url === pathToFileURL(realpathSync(script)).href;
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ }
62
+ if (estPrincipal()) {
63
+ try {
64
+ process.exitCode = await executerCli(process.argv.slice(2), process.cwd());
65
+ }
66
+ catch (e) {
67
+ console.error(e instanceof Error ? e.message : String(e));
68
+ process.exitCode = 1;
69
+ }
70
+ }
@@ -0,0 +1,38 @@
1
+ import { z } from 'astro/zod';
2
+ import { glob } from 'astro/loaders';
3
+ import type { DeclCollection, DeclSingleton } from './types.js';
4
+ /** Clé symbole portant les métadonnées CMS sur l'objet collection (invisible pour Astro). */
5
+ export declare const CMS: unique symbol;
6
+ type Loader = ReturnType<typeof glob>;
7
+ type Forme = Record<string, z.ZodType>;
8
+ type ChampOrdre = {
9
+ ordre: z.ZodInt;
10
+ };
11
+ /** Schéma strict d'une collection : sa forme, plus `ordre` si elle est ordonnable. */
12
+ export type SchemaCollection<S extends Forme, Ord extends boolean> = z.ZodObject<Ord extends true ? S & ChampOrdre : S, z.core.$strict>;
13
+ /** Type que pose `defineCollection` d'astro:content ; Astro refuse une collection à loader sans lui. */
14
+ declare const TYPE_ASTRO = "content_layer";
15
+ export declare function defineSingleton<S extends Forme>(id: string, o: {
16
+ libelle: string;
17
+ adresse?: string;
18
+ schema: S;
19
+ }): {
20
+ type: typeof TYPE_ASTRO;
21
+ loader: Loader;
22
+ schema: z.ZodObject<S, z.core.$strict>;
23
+ [CMS]: DeclSingleton;
24
+ };
25
+ export declare function defineCollection<S extends Forme, Ord extends boolean = false>(id: string, o: {
26
+ libelle: string;
27
+ format: 'json' | 'markdown';
28
+ titre: string;
29
+ ordonnable?: Ord;
30
+ schema: S;
31
+ }): {
32
+ type: typeof TYPE_ASTRO;
33
+ loader: Loader;
34
+ schema: SchemaCollection<S, Ord>;
35
+ [CMS]: DeclCollection;
36
+ };
37
+ export declare function lireDecl(c: unknown): DeclSingleton | DeclCollection | undefined;
38
+ export {};
@@ -0,0 +1,52 @@
1
+ import { z } from 'astro/zod';
2
+ import { glob } from 'astro/loaders';
3
+ import { lireMeta } from './types.js';
4
+ /** Clé symbole portant les métadonnées CMS sur l'objet collection (invisible pour Astro). */
5
+ export const CMS = Symbol.for('komekoo-cms.decl');
6
+ /** Type que pose `defineCollection` d'astro:content ; Astro refuse une collection à loader sans lui. */
7
+ const TYPE_ASTRO = 'content_layer';
8
+ const RE_ADRESSE = /^\/[a-z0-9\-\/]*$/;
9
+ const RE_ID = /^[a-z][a-z0-9-]*$/;
10
+ function verifierId(id) {
11
+ if (!RE_ID.test(id)) {
12
+ throw new Error(`id « ${id} » invalide : attendu ${RE_ID.source}`);
13
+ }
14
+ }
15
+ export function defineSingleton(id, o) {
16
+ verifierId(id);
17
+ if (o.adresse !== undefined && !RE_ADRESSE.test(o.adresse)) {
18
+ throw new Error(`adresse « ${o.adresse} » invalide : attendu ${RE_ADRESSE.source}`);
19
+ }
20
+ const decl = { type: 'singleton', id, libelle: o.libelle };
21
+ if (o.adresse !== undefined)
22
+ decl.adresse = o.adresse;
23
+ return {
24
+ type: TYPE_ASTRO,
25
+ loader: glob({ pattern: `${id}.json`, base: './src/content/pages' }),
26
+ schema: z.strictObject(o.schema),
27
+ [CMS]: decl,
28
+ };
29
+ }
30
+ export function defineCollection(id, o) {
31
+ verifierId(id);
32
+ if (id === 'pages') {
33
+ throw new Error("l'id « pages » est réservé aux singletons");
34
+ }
35
+ const champTitre = o.schema[o.titre];
36
+ if (!champTitre || lireMeta(champTitre)?.editeur !== 'texte') {
37
+ throw new Error(`titre « ${o.titre} » doit désigner un champ existant de type texte`);
38
+ }
39
+ const ordonnable = o.ordonnable ?? false;
40
+ const forme = ordonnable ? { ...o.schema, ordre: z.int().min(0) } : o.schema;
41
+ return {
42
+ type: TYPE_ASTRO,
43
+ loader: glob({ pattern: o.format === 'json' ? '**/*.json' : '**/*.md', base: `./src/content/${id}` }),
44
+ schema: z.strictObject(forme),
45
+ [CMS]: { type: 'collection', id, libelle: o.libelle, format: o.format, titre: o.titre, ordonnable },
46
+ };
47
+ }
48
+ export function lireDecl(c) {
49
+ if (typeof c !== 'object' || c === null)
50
+ return undefined;
51
+ return c[CMS];
52
+ }
@@ -0,0 +1,8 @@
1
+ export declare const LARGEURS: readonly [800, 1600, 2400];
2
+ export type Variante = {
3
+ largeur: (typeof LARGEURS)[number];
4
+ url: string;
5
+ };
6
+ export declare function variantes(src: string): Variante[];
7
+ /** Variantes strictement plus petites que l'original, puis le fichier -2400 (original non agrandi). */
8
+ export declare function srcsetDe(src: string, largeurOriginale: number): string;
package/dist/images.js ADDED
@@ -0,0 +1,19 @@
1
+ import { MOTIF_SRC_IMAGE } from './champs.js';
2
+ export const LARGEURS = [800, 1600, 2400];
3
+ export function variantes(src) {
4
+ if (!MOTIF_SRC_IMAGE.test(src)) {
5
+ throw new Error(`Chemin d'image invalide : ${src}`);
6
+ }
7
+ const base = src.slice(0, -'-2400.webp'.length);
8
+ return LARGEURS.map((largeur) => ({ largeur, url: `${base}-${largeur}.webp` }));
9
+ }
10
+ /** Variantes strictement plus petites que l'original, puis le fichier -2400 (original non agrandi). */
11
+ export function srcsetDe(src, largeurOriginale) {
12
+ const toutes = variantes(src);
13
+ const plusGrande = toutes[toutes.length - 1];
14
+ const entrees = toutes
15
+ .filter((v) => v !== plusGrande && v.largeur < largeurOriginale)
16
+ .map((v) => `${v.url} ${v.largeur}w`);
17
+ entrees.push(`${plusGrande.url} ${Math.min(largeurOriginale, 2400)}w`);
18
+ return entrees.join(', ');
19
+ }
@@ -0,0 +1,12 @@
1
+ export declare const VERSION_SCHEMA = 1;
2
+ export { champ } from './champs.js';
3
+ export { lireMeta } from './types.js';
4
+ export type { Editeur, MetaCms, OptionsBase, DeclSingleton, DeclCollection } from './types.js';
5
+ export { ErreurCms } from './noyau/erreurs.js';
6
+ export { defineSingleton, defineCollection, lireDecl, CMS } from './collections.js';
7
+ export { creerAjv, MOTS_CLES_CMS } from './ajv.js';
8
+ export { versManifest } from './noyau/manifest.js';
9
+ export type { OptionsCms } from './noyau/manifest.js';
10
+ export { HORS_PERIMETRE_SOCLE } from './socle/hors-perimetre.js';
11
+ export { LARGEURS, variantes, srcsetDe } from './images.js';
12
+ export type { Variante } from './images.js';
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export const VERSION_SCHEMA = 1;
2
+ export { champ } from './champs.js';
3
+ export { lireMeta } from './types.js';
4
+ export { ErreurCms } from './noyau/erreurs.js';
5
+ export { defineSingleton, defineCollection, lireDecl, CMS } from './collections.js';
6
+ export { creerAjv, MOTS_CLES_CMS } from './ajv.js';
7
+ export { versManifest } from './noyau/manifest.js';
8
+ export { HORS_PERIMETRE_SOCLE } from './socle/hors-perimetre.js';
9
+ export { LARGEURS, variantes, srcsetDe } from './images.js';
@@ -0,0 +1,9 @@
1
+ import type { AstroIntegration } from 'astro';
2
+ import type { OptionsCms } from './noyau/manifest.js';
3
+ /** `Symbol.for` : la même clé que l'intégration soit chargée par Node ou par Vite (`chargerOptions`). */
4
+ export declare const OPTIONS: unique symbol;
5
+ export default function cms(options: OptionsCms): AstroIntegration & {
6
+ [OPTIONS]: OptionsCms;
7
+ };
8
+ /** Retrouve les options passées à `cms()` dans `astro.config.mjs` (utilisé par la CLI). */
9
+ export declare function chargerOptions(racine: string): Promise<OptionsCms>;
@@ -0,0 +1,91 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { chargerConfigAstro } from './noyau/charger.js';
4
+ import { formaterViolations } from './noyau/contenus.js';
5
+ import { MOTIF_SRC_IMAGE, MOTIF_URL_SITE } from './motifs.js';
6
+ import { ErreurCms } from './noyau/erreurs.js';
7
+ import { synchroniser, verifier } from './noyau/operations.js';
8
+ import { ecrireVersion } from './noyau/version.js';
9
+ const NOM = '@komekoo/astro-cms';
10
+ /** `Symbol.for` : la même clé que l'intégration soit chargée par Node ou par Vite (`chargerOptions`). */
11
+ export const OPTIONS = Symbol.for('@komekoo/astro-cms/options');
12
+ const estTexte = (v) => typeof v === 'string' && v.trim() !== '';
13
+ /** Options passées depuis `astro.config.mjs` (JavaScript, non typé) : vérifiées à l'appel de `cms()`. */
14
+ function verifierOptions(options) {
15
+ const o = (typeof options === 'object' && options !== null ? options : {});
16
+ for (const cle of ['site', 'contact']) {
17
+ if (!estTexte(o[cle]))
18
+ throw new ErreurCms(`${NOM} : option « ${cle} » obligatoire (texte non vide)`, 'astro.config.mjs');
19
+ }
20
+ const hp = o.horsPerimetre;
21
+ if (hp !== undefined && !(Array.isArray(hp) && hp.every((x) => typeof x === 'string'))) {
22
+ throw new ErreurCms(`${NOM} : option « horsPerimetre » doit être une liste de textes`, 'astro.config.mjs');
23
+ }
24
+ const refuser = (cle, attendu) => {
25
+ throw new ErreurCms(`${NOM} : option « ${cle} » ${attendu}`, 'astro.config.mjs');
26
+ };
27
+ if (!estTexte(o.url) || !MOTIF_URL_SITE.test(o.url) || o.url.endsWith('/')) {
28
+ refuser('url', 'obligatoire : adresse https sans « / » final (ex. https://exemple.fr)');
29
+ }
30
+ const admin = o.admin;
31
+ if (admin === undefined)
32
+ return;
33
+ if (typeof admin !== 'object' || admin === null || Array.isArray(admin)) {
34
+ return refuser('admin', 'doit être un objet { accent?, logo?, origine? }');
35
+ }
36
+ const { accent, logo, origine } = admin;
37
+ if (accent !== undefined && !(typeof accent === 'string' && /^#[0-9A-Fa-f]{6}$/.test(accent))) {
38
+ refuser('admin.accent', 'doit être une couleur au format #RRGGBB');
39
+ }
40
+ if (logo !== undefined && !(typeof logo === 'string' && MOTIF_SRC_IMAGE.test(logo))) {
41
+ refuser('admin.logo', 'doit être une image téléversée (/uploads/…-2400.webp)');
42
+ }
43
+ if (origine !== undefined && !(typeof origine === 'string' && /^https:\/\/[^\s/]+$/.test(origine))) {
44
+ refuser('admin.origine', 'doit être une origine https sans chemin (ex. https://admin.exemple.fr)');
45
+ }
46
+ }
47
+ export default function cms(options) {
48
+ verifierOptions(options);
49
+ return {
50
+ name: NOM,
51
+ [OPTIONS]: options,
52
+ hooks: {
53
+ 'astro:config:setup': async ({ config, command, addWatchFile, logger }) => {
54
+ const racine = fileURLToPath(config.root);
55
+ if (command === 'dev' || command === 'sync')
56
+ await synchroniser(racine, options);
57
+ if (command === 'dev')
58
+ addWatchFile(path.join(racine, 'src/content.config.ts'));
59
+ // Avant la synchronisation de contenu d'Astro (glob loader), qui s'arrête à la première erreur Zod.
60
+ if (command === 'build') {
61
+ const violations = await verifier(racine, options);
62
+ if (violations.length === 0)
63
+ return;
64
+ logger.error(formaterViolations(violations));
65
+ throw new ErreurCms(`${violations.length} problème(s) de contenu`);
66
+ }
67
+ },
68
+ 'astro:build:done': async ({ dir }) => {
69
+ await ecrireVersion(fileURLToPath(dir), process.env.CF_PAGES_COMMIT_SHA ?? 'inconnu', options.admin?.origine ?? 'https://cms.komekoo.link');
70
+ },
71
+ },
72
+ };
73
+ }
74
+ function aplatir(valeur) {
75
+ return Array.isArray(valeur) ? valeur.flatMap(aplatir) : [valeur];
76
+ }
77
+ /** Retrouve les options passées à `cms()` dans `astro.config.mjs` (utilisé par la CLI). */
78
+ export async function chargerOptions(racine) {
79
+ const config = await chargerConfigAstro(racine);
80
+ const integrations = config?.integrations;
81
+ for (const integration of aplatir(integrations)) {
82
+ if (typeof integration !== 'object' || integration === null)
83
+ continue;
84
+ if (integration.name !== NOM)
85
+ continue;
86
+ const options = integration[OPTIONS];
87
+ if (options !== undefined)
88
+ return options;
89
+ }
90
+ throw new ErreurCms(`intégration ${NOM} absente de astro.config.mjs`);
91
+ }
@@ -0,0 +1,8 @@
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;
package/dist/motifs.js ADDED
@@ -0,0 +1,9 @@
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?#]*)?$/;
@@ -0,0 +1,5 @@
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_LIEN, MOTIF_SRC_IMAGE, MOTIF_URL_SITE } from './motifs.js';
@@ -0,0 +1,5 @@
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_LIEN, MOTIF_SRC_IMAGE, MOTIF_URL_SITE } from './motifs.js';
@@ -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>;