From 816e15aa62830a1867578dadfa554faae79ef074 Mon Sep 17 00:00:00 2001 From: LBU Date: Mon, 17 Aug 2026 18:18:19 +0200 Subject: [PATCH 1/3] FSHSP-125 feat(schematics): copy each component's story and MDX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Un projet en sources copiées possédait son code mais aucune doc : il lui restait un Storybook hébergé qui décrit nos composants, pas ses copies éditées. `--with-storybook` pose désormais, à côté de chaque composant, sa story et sa page MDX, plus la chaîne qui les alimente — `docs.config.mjs` et le bloc `` — pour que les tables « Theming » lisent les `.scss` du projet et décrivent ses valeurs. Les imports suivent les copies, comme pour les sources. Le MDX passe par une réécriture propre : elle ignore les blocs de code, qui sont des exemples destinés au lecteur et non des imports à résoudre — les réécrire produirait des chemins relatifs faux, dépendants d'un fichier que nous ne connaissons pas. `docs.config.mjs` est copié tel quel et reconnaît seul sa disposition, monorepo ou projet consommateur : un seul script à maintenir. Désactivé par défaut, parce que sans Storybook installé ce sont des fichiers qui importent des packages absents. Le choix est retenu dans `ui-kit.json`, d'où `update` le relit — sans quoi une mise à jour poserait de la doc chez qui n'en veut pas. La configuration Storybook elle-même reste à poser. Vérifié sur un arbre consommateur complet : 53 composants copiés, 53 MDX compilés par le loader de Storybook, 55 composants relevés par la chaîne de doc, aucun import du kit résiduel. --- CHANGELOG.md | 5 ++ projects/ui-kit-schematics/README.fr.md | 14 ++++ projects/ui-kit-schematics/README.md | 13 ++++ projects/ui-kit-schematics/src/add/index.ts | 12 ++- .../ui-kit-schematics/src/add/schema.d.ts | 3 + .../ui-kit-schematics/src/add/schema.json | 4 + .../ui-kit-schematics/src/ng-add/index.ts | 45 +++++++++-- .../ui-kit-schematics/src/ng-add/schema.d.ts | 2 + .../ui-kit-schematics/src/ng-add/schema.json | 5 ++ .../ui-kit-schematics/src/update/index.ts | 5 +- .../src/utils/component-registry.ts | 15 ++++ projects/ui-kit-schematics/src/utils/copy.ts | 26 +++++-- .../ui-kit-schematics/src/utils/export-map.ts | 4 + .../ui-kit-schematics/src/utils/manifest.ts | 9 ++- .../src/utils/rewrite-imports.ts | 75 ++++++++++++++++++- scripts/docs.config.mjs | 45 +++++++++-- scripts/schematics-assets.build.mjs | 38 ++++++++-- 17 files changed, 287 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55390e9..8ed58a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le pr ## [Unreleased] ### Added +- **Les composants copiés peuvent emporter leur doc** (FSHSP-125) : `ng add @4sh/ui-kit-schematics --with-storybook` copie, à côté de chaque composant, sa story et sa page MDX, et pose la chaîne qui les alimente (`scripts/docs.config.mjs`, bloc ``). Jusqu'ici un projet en sources copiées possédait son code mais aucune doc : il lui restait un Storybook hébergé qui décrit *nos* composants, pas ses copies éditées. + + Les imports suivent les copies, comme pour les sources : la story désigne ses voisins locaux, le MDX désigne le bloc `` posé dans le projet — mais les extraits de code des pages, eux, ne sont pas réécrits (ce sont des exemples destinés au lecteur, pas des imports à résoudre). Les tables *Theming* restent lues sur les `.scss` du projet : elles décrivent ses valeurs, pas les nôtres. + + Désactivé par défaut : sans Storybook installé, ce sont des fichiers qui importent des packages absents. Le choix est retenu dans `ui-kit.json` et `update` s'y tient. La configuration Storybook elle-même n'est pas encore posée par le schematic. - **`@4sh/ui-kit-schematics` embarque son `LICENSE` et ses README** (EN + FR). Le package déclarait `Apache-2.0` sans en fournir le texte, alors que la licence (§4a) demande qu'une redistribution en joigne une copie — d'autant plus ici que ce package existe pour que son contenu soit recopié ailleurs. Sa page npmjs, jusque-là vide, redirige maintenant vers `@4sh/ui-kit` : le compagnon ne s'installe jamais directement. ### Fixed diff --git a/projects/ui-kit-schematics/README.fr.md b/projects/ui-kit-schematics/README.fr.md index 0cee2cf..64d4786 100644 --- a/projects/ui-kit-schematics/README.fr.md +++ b/projects/ui-kit-schematics/README.fr.md @@ -37,9 +37,23 @@ jamais une fusion automatique. | `ng add @4sh/ui-kit-schematics` | fondation **et** composants, d'un coup | | `ng add @4sh/ui-kit-schematics --skip-components` | fondation seule, composants choisis plus tard | | `ng add @4sh/ui-kit-schematics --skip-install` | ne pas lancer `npm install` (projet qui pilote son lockfile) | +| `ng add @4sh/ui-kit-schematics --with-storybook` | copier aussi la story et le MDX de chaque composant (voir ci-dessous) | | `ng generate @4sh/ui-kit-schematics:add` | copier d'autres composants (interactif, ou `--components`, ou `--all`) | | `ng generate @4sh/ui-kit-schematics:update` | diff des composants copiés face aux sources publiées | +### Documenter ses copies : `--with-storybook` + +Désactivé par défaut. Activé, chaque composant arrive avec sa story et sa page +MDX, et le projet reçoit la chaîne qui garde leurs tables *Theming* justes : +`scripts/docs.config.mjs` lit les rôles `///` de vos `.scss`, si bien que les +tables décrivent **vos** valeurs — la raison même de copier les sources. Le choix +est retenu dans `ui-kit.json`, et `update` s'y tient. + +Ce que cela ne fait **pas** encore : installer Storybook ni écrire sa +configuration. Pointez votre `storybook/main.js` sur +`src/app/shared/components/**/*.mdx` et `**/*.stories.ts`, et lancez +`npm run docs:config` avant de le démarrer. + ### `@4sh/ui-kit` n'est délibérément **pas** installé Cette voie ne met jamais le kit dans `node_modules`, et c'est précisément le but : diff --git a/projects/ui-kit-schematics/README.md b/projects/ui-kit-schematics/README.md index ed68645..6d2a818 100644 --- a/projects/ui-kit-schematics/README.md +++ b/projects/ui-kit-schematics/README.md @@ -35,9 +35,22 @@ against newer sources — never an automatic merge. | `ng add @4sh/ui-kit-schematics` | foundation **and** components, in one go | | `ng add @4sh/ui-kit-schematics --skip-components` | foundation only, pick components later | | `ng add @4sh/ui-kit-schematics --skip-install` | skip `npm install` (project drives its own lockfile) | +| `ng add @4sh/ui-kit-schematics --with-storybook` | also copy each component's story and MDX (see below) | | `ng generate @4sh/ui-kit-schematics:add` | copy more components (interactive, or `--components`, or `--all`) | | `ng generate @4sh/ui-kit-schematics:update` | diff copied components against the published sources | +### Documenting your copies: `--with-storybook` + +Off by default. Turned on, each component arrives with its story and its MDX page, +and the project gets the chain that keeps their *Theming* tables truthful: +`scripts/docs.config.mjs` reads the `///` roles out of your `.scss` files, so the +tables describe **your** values — the point of copying sources in the first place. +The choice is recorded in `ui-kit.json`, and `update` honours it. + +What this does **not** do yet: install Storybook or write its configuration. Point +your own `storybook/main.js` at `src/app/shared/components/**/*.mdx` and +`**/*.stories.ts`, and run `npm run docs:config` before starting it. + ### `@4sh/ui-kit` is deliberately **not** installed This path never puts the kit in `node_modules`, and that is the point: with it diff --git a/projects/ui-kit-schematics/src/add/index.ts b/projects/ui-kit-schematics/src/add/index.ts index b78ddb3..9e23c8c 100644 --- a/projects/ui-kit-schematics/src/add/index.ts +++ b/projects/ui-kit-schematics/src/add/index.ts @@ -51,18 +51,24 @@ export function add(options: Schema): Rule { const kitVersion = readKitVersion(); const units = resolveDependencies(selected); - const manifest = readManifest(tree) ?? emptyManifest(kitVersion); + const existing = readManifest(tree); + // `--with-storybook` n'est passé qu'à l'installation : sur un `add` isolé, + // c'est le manifeste qui porte le choix du projet. + const withStorybook = options.withStorybook ?? existing?.storybook ?? false; + const manifest = existing ?? emptyManifest(kitVersion, withStorybook); for (const unit of units) { - copyUnit(tree, unit, kitVersion); + copyUnit(tree, unit, kitVersion, { withStorybook }); manifest.components[unit.name] = { version: kitVersion, installedAt: today() }; } manifest.kitVersion = kitVersion; + manifest.storybook = withStorybook; writeManifest(tree, manifest); const extra = units.filter((u) => !selected.includes(u.name)).map((u) => u.name); context.logger.info( - `✔ ${units.length} unité(s) copiée(s)${extra.length ? ` (dont dépendances : ${extra.join(', ')})` : ''}.`, + `✔ ${units.length} unité(s) copiée(s)${extra.length ? ` (dont dépendances : ${extra.join(', ')})` : ''}` + + `${withStorybook ? ', story et MDX inclus' : ''}.`, ); return tree; }; diff --git a/projects/ui-kit-schematics/src/add/schema.d.ts b/projects/ui-kit-schematics/src/add/schema.d.ts index e07334f..b0f8f0d 100644 --- a/projects/ui-kit-schematics/src/add/schema.d.ts +++ b/projects/ui-kit-schematics/src/add/schema.d.ts @@ -3,4 +3,7 @@ export interface Schema { components?: string[]; /** Copie tous les composants disponibles, sans prompt. */ all?: boolean; + /** Copie aussi la story et le MDX de chaque composant. Omis → on reconduit le + * choix mémorisé dans `ui-kit.json` à l'installation. */ + withStorybook?: boolean; } diff --git a/projects/ui-kit-schematics/src/add/schema.json b/projects/ui-kit-schematics/src/add/schema.json index a33f73c..be35cd0 100644 --- a/projects/ui-kit-schematics/src/add/schema.json +++ b/projects/ui-kit-schematics/src/add/schema.json @@ -14,6 +14,10 @@ "type": "boolean", "default": false, "description": "Copie tous les composants disponibles, sans prompt." + }, + "withStorybook": { + "type": "boolean", + "description": "Copie aussi la story et le MDX de chaque composant. Omis → choix mémorisé dans ui-kit.json." } } } diff --git a/projects/ui-kit-schematics/src/ng-add/index.ts b/projects/ui-kit-schematics/src/ng-add/index.ts index 336da37..c74d35e 100644 --- a/projects/ui-kit-schematics/src/ng-add/index.ts +++ b/projects/ui-kit-schematics/src/ng-add/index.ts @@ -18,7 +18,7 @@ import { join } from 'node:path'; import type { Schema } from './schema'; import { addDependency, addNpmScript, readPackageJson } from '../utils/package-json'; import { emptyManifest, MANIFEST_PATH, writeManifest } from '../utils/manifest'; -import { stylesFoundationDir } from '../utils/component-registry'; +import { CONFIG_TABLE_PATH, docsPipelineDir, stylesFoundationDir } from '../utils/component-registry'; import { readKitManifestInfo } from '../utils/kit-manifest'; import { add } from '../add'; @@ -146,6 +146,32 @@ function copyTokensPipeline(): Rule { }; } +/** + * Chaîne de doc (FSHSP-125), pendant de `copyTokensPipeline` : le bloc + * `` qu'importe chaque MDX copié, et le script qui lui produit son + * manifeste depuis les `///` des `.scss` du projet. + * + * Sans elle, la section « Theming » de chaque page décrirait les valeurs du kit + * au jour de la copie, pas celles du projet — or c'est précisément ce que ce + * système existe pour éviter. Le script est copié tel quel : il reconnaît seul + * la disposition d'un projet consommateur. + */ +function copyDocsPipeline(): Rule { + return (tree: Tree, context: SchematicContext) => { + const dir = docsPipelineDir(); + for (const [name, targetPath] of [ + ['docs.config.mjs', 'scripts/docs.config.mjs'], + ['config-table.js', CONFIG_TABLE_PATH], + ] as const) { + if (tree.exists(targetPath)) continue; // éditable : jamais réécrasé + tree.create(targetPath, readFileSync(join(dir, name), 'utf8')); + } + addNpmScript(tree, 'docs:config', 'node scripts/docs.config.mjs'); + context.logger.info(`✔ Chaîne de doc copiée (scripts/docs.config.mjs, ${CONFIG_TABLE_PATH}).`); + return tree; + }; +} + function addRuntimeDependencies(): Rule { return (tree: Tree, context: SchematicContext) => { const { peerDependencies } = readKitManifestInfo(); @@ -179,11 +205,11 @@ function addRuntimeDependencies(): Rule { }; } -function createManifest(): Rule { +function createManifest(withStorybook: boolean): Rule { return (tree: Tree, context: SchematicContext) => { if (tree.exists(MANIFEST_PATH)) return tree; // ré-exécution de `ng add` : on ne réinitialise pas const { version } = readKitManifestInfo(); - writeManifest(tree, emptyManifest(version)); + writeManifest(tree, emptyManifest(version, withStorybook)); context.logger.info(`✔ ${MANIFEST_PATH} créé (kitVersion: ${version}).`); return tree; }; @@ -207,13 +233,18 @@ function installRuntimeDependencies(): Rule { } export function ngAdd(options: Schema): Rule { + const withStorybook = options.withStorybook ?? false; + const foundation = [ copyStylesFoundationRule(), createStyleScaffolds(), copyTokensPipeline(), + // La chaîne de doc ne sert qu'aux MDX copiés : sans eux, ce sont deux + // fichiers morts dans le dépôt du consommateur. + ...(withStorybook ? [copyDocsPipeline()] : []), addRuntimeDependencies(), updateAngularJson(), - createManifest(), + createManifest(withStorybook), ]; // En queue de chaîne : la tâche s'exécute après application de l'arbre, donc @@ -237,5 +268,9 @@ export function ngAdd(options: Schema): Rule { ]); } - return chain([...foundation, add({ components: options.components, all: options.all }), ...install]); + return chain([ + ...foundation, + add({ components: options.components, all: options.all, withStorybook }), + ...install, + ]); } diff --git a/projects/ui-kit-schematics/src/ng-add/schema.d.ts b/projects/ui-kit-schematics/src/ng-add/schema.d.ts index 4607aae..34a53e8 100644 --- a/projects/ui-kit-schematics/src/ng-add/schema.d.ts +++ b/projects/ui-kit-schematics/src/ng-add/schema.d.ts @@ -6,4 +6,6 @@ export interface Schema { all?: boolean; /** Pose la fondation seule, sans copier de composant. */ skipComponents?: boolean; + /** Copie la doc des composants (story + MDX) et la chaîne qui l'alimente. */ + withStorybook?: boolean; } diff --git a/projects/ui-kit-schematics/src/ng-add/schema.json b/projects/ui-kit-schematics/src/ng-add/schema.json index 2c845b1..3c2ada0 100644 --- a/projects/ui-kit-schematics/src/ng-add/schema.json +++ b/projects/ui-kit-schematics/src/ng-add/schema.json @@ -23,6 +23,11 @@ "type": "boolean", "default": false, "description": "Pose la fondation seule. Les composants se copient ensuite avec `ng generate @4sh/ui-kit-schematics:add`." + }, + "withStorybook": { + "type": "boolean", + "default": false, + "description": "Copie la story et le MDX de chaque composant, plus la chaîne de doc (docs.config.mjs, bloc ConfigTable). La configuration Storybook elle-même reste à poser." } } } diff --git a/projects/ui-kit-schematics/src/update/index.ts b/projects/ui-kit-schematics/src/update/index.ts index c92b00b..b7a2674 100644 --- a/projects/ui-kit-schematics/src/update/index.ts +++ b/projects/ui-kit-schematics/src/update/index.ts @@ -58,7 +58,10 @@ export function update(options: Schema): Rule { context.logger.warn(`${name} : présent dans ui-kit.json mais introuvable dans le kit installé — ignoré.`); continue; } - const files = renderUnitFiles(unit, kitVersion); + // Le choix fait à l'installation, pas une valeur par défaut : une mise à + // jour ne doit ni introduire de la doc chez qui n'en a pas demandé, ni + // laisser périmée celle du projet qui en a (FSHSP-125). + const files = renderUnitFiles(unit, kitVersion, { withStorybook: manifest.storybook ?? false }); while (true) { diff --git a/projects/ui-kit-schematics/src/utils/component-registry.ts b/projects/ui-kit-schematics/src/utils/component-registry.ts index c96dfbc..3b06741 100644 --- a/projects/ui-kit-schematics/src/utils/component-registry.ts +++ b/projects/ui-kit-schematics/src/utils/component-registry.ts @@ -24,6 +24,11 @@ const COMPONENTS_ROOT = 'src/app/shared/components/ui'; /** Directives de base, services, utilitaires et types transverses : hors de * `components/`, qui ne doit contenir que des composants (FSHSP-121). */ const CORE_ROOT = 'src/app/shared/ui-core'; +/** Bloc de doc `` chez le consommateur (FSHSP-125) : sa place + * décide du chemin relatif réécrit dans chaque MDX copié, d'où la constante + * partagée plutôt qu'un littéral des deux côtés. Même disposition qu'ici, pour + * que le scaffold Storybook à venir tombe juste autour. */ +export const CONFIG_TABLE_PATH = 'storybook/blocks/config-table.js'; function listDirs(dir: string): string[] { if (!existsSync(dir)) return []; @@ -82,6 +87,16 @@ export function stylesFoundationDir(): string { return join(ASSETS_ROOT, 'styles'); } +/** Chaîne de doc embarquée : `docs.config.mjs` + le bloc `` (FSHSP-125). */ +export function docsPipelineDir(): string { + return join(ASSETS_ROOT, 'docs-pipeline'); +} + +/** Story et MDX d'un composant : sa doc, copiée seulement sur `--with-storybook`. */ +export function isStorybookFile(path: string): boolean { + return path.endsWith('.stories.ts') || path.endsWith('.mdx'); +} + /** Tous les fichiers d'une unité, chemins absolus. */ export function unitSourceFiles(unit: AssetUnit): string[] { const out: string[] = []; diff --git a/projects/ui-kit-schematics/src/utils/copy.ts b/projects/ui-kit-schematics/src/utils/copy.ts index 9312e0d..b2419f5 100644 --- a/projects/ui-kit-schematics/src/utils/copy.ts +++ b/projects/ui-kit-schematics/src/utils/copy.ts @@ -8,14 +8,17 @@ import { SchematicsException } from '@angular-devkit/schematics'; import { readFileSync } from 'node:fs'; import { basename } from 'node:path'; import type { AssetUnit } from './component-registry'; -import { flattenedRelPath, unitSourceFiles } from './component-registry'; +import { flattenedRelPath, isStorybookFile, unitSourceFiles } from './component-registry'; import { BARREL_FILENAME } from './export-map'; -import { rewriteKitImports } from './rewrite-imports'; +import { rewriteDocImports, rewriteKitImports } from './rewrite-imports'; const HEADER_STYLE: Record string> = { '.ts': (lines) => lines.map((l) => `// ${l}`).join('\n') + '\n', '.scss': (lines) => lines.map((l) => `// ${l}`).join('\n') + '\n', '.html': (lines) => `\n`, + // Commentaire d'expression JSX : le MDX v3 ne connaît plus ``, qu'il + // rendrait tel quel en haut de la page de doc. + '.mdx': (lines) => `{/*\n${lines.map((l) => ` ${l}`).join('\n')}\n*/}\n\n`, }; function traceabilityHeader(unit: AssetUnit, relPath: string, kitVersion: string, ext: string): string { @@ -37,9 +40,17 @@ export interface RenderedFile { content: string; } +export interface RenderOptions { + /** Poser aussi la story et le MDX du composant (FSHSP-125). Par défaut non : + * sans Storybook dans le projet, ce sont deux fichiers qui importent des + * packages absents. Le choix est mémorisé dans `ui-kit.json`, pour qu'`update` + * le reconduise au lieu de le redemander. */ + withStorybook?: boolean; +} + /** Calcule le contenu final (en-tête inclus) de chaque fichier d'une unité, * sans rien écrire — réutilisé par `copyUnit` et par le diff d'`update`. */ -export function renderUnitFiles(unit: AssetUnit, kitVersion: string): RenderedFile[] { +export function renderUnitFiles(unit: AssetUnit, kitVersion: string, options: RenderOptions = {}): RenderedFile[] { const files: RenderedFile[] = []; const unresolved: string[] = []; @@ -55,6 +66,7 @@ export function renderUnitFiles(unit: AssetUnit, kitVersion: string): RenderedFi // Comparaison sur le nom EXACT : un `endsWith` écarterait aussi un // `ui-table-public-api.ts`, qui est un fichier de composant ordinaire. if (basename(absSrc) === BARREL_FILENAME) continue; + if (!options.withStorybook && isStorybookFile(absSrc)) continue; const relPath = flattenedRelPath(unit, absSrc); const ext = absSrc.slice(absSrc.lastIndexOf('.')); @@ -70,8 +82,8 @@ export function renderUnitFiles(unit: AssetUnit, kitVersion: string): RenderedFi claimedBy.set(targetPath, absSrc); let source = readFileSync(absSrc, 'utf8'); - if (ext === '.ts') { - const result = rewriteKitImports(source, targetPath); + if (ext === '.ts' || ext === '.mdx') { + const result = ext === '.mdx' ? rewriteDocImports(source, targetPath) : rewriteKitImports(source, targetPath); source = result.content; unresolved.push(...result.unresolved.map((item) => `${relPath} → ${item}`)); } @@ -94,9 +106,9 @@ export function renderUnitFiles(unit: AssetUnit, kitVersion: string): RenderedFi } /** Copie tous les fichiers d'une unité dans l'arbre, avec en-tête de traçabilité. */ -export function copyUnit(tree: Tree, unit: AssetUnit, kitVersion: string): string[] { +export function copyUnit(tree: Tree, unit: AssetUnit, kitVersion: string, options: RenderOptions = {}): string[] { const written: string[] = []; - for (const { targetPath, content } of renderUnitFiles(unit, kitVersion)) { + for (const { targetPath, content } of renderUnitFiles(unit, kitVersion, options)) { if (tree.exists(targetPath)) tree.overwrite(targetPath, content); else tree.create(targetPath, content); written.push(targetPath); diff --git a/projects/ui-kit-schematics/src/utils/export-map.ts b/projects/ui-kit-schematics/src/utils/export-map.ts index 32d0c18..d8b46fa 100644 --- a/projects/ui-kit-schematics/src/utils/export-map.ts +++ b/projects/ui-kit-schematics/src/utils/export-map.ts @@ -80,6 +80,10 @@ export function buildExportMap(unit: AssetUnit): Map { continue; } if (!absPath.endsWith('.ts')) continue; + // Une story exporte des symboles (`export const High: Story`) qui ne sont + // pas de l'API : les indexer ferait réadresser un import du kit vers un + // fichier de doc si les deux noms se croisaient (FSHSP-125). + if (absPath.endsWith('.stories.ts')) continue; // Chemin aplati, sans extension — c'est ce qu'un import doit désigner. const flattened = flattenedRelPath(unit, absPath).replace(/\.ts$/, ''); diff --git a/projects/ui-kit-schematics/src/utils/manifest.ts b/projects/ui-kit-schematics/src/utils/manifest.ts index 8a0dabd..1c29226 100644 --- a/projects/ui-kit-schematics/src/utils/manifest.ts +++ b/projects/ui-kit-schematics/src/utils/manifest.ts @@ -24,6 +24,11 @@ export interface ManifestComponentEntry { export interface UiKitManifest { kitVersion: string; + /** Le projet reçoit-il la story et le MDX de chaque composant copié + * (FSHSP-125) ? Mémorisé ici parce qu'`update` doit reconduire le choix fait + * à l'installation : sans lui, une mise à jour poserait de la doc chez qui + * n'en veut pas, ou l'effacerait chez qui en a. */ + storybook?: boolean; components: Record; } @@ -42,8 +47,8 @@ export function writeManifest(tree: Tree, manifest: UiKitManifest): void { } } -export function emptyManifest(kitVersion: string): UiKitManifest { - return { kitVersion, components: {} }; +export function emptyManifest(kitVersion: string, storybook = false): UiKitManifest { + return { kitVersion, storybook, components: {} }; } /** Horodatage ISO, tronqué au jour — cohérent avec l'exemple du ticket (`"2026-08-13"`). */ diff --git a/projects/ui-kit-schematics/src/utils/rewrite-imports.ts b/projects/ui-kit-schematics/src/utils/rewrite-imports.ts index 8bc4352..e076b92 100644 --- a/projects/ui-kit-schematics/src/utils/rewrite-imports.ts +++ b/projects/ui-kit-schematics/src/utils/rewrite-imports.ts @@ -7,6 +7,10 @@ * aucun effet sur les composants qui l'utilisent, et le modèle « les sources * t'appartiennent » ne tiendrait pas. C'est la raison d'être du starter. * + * Les MDX passent par la même porte ({@link rewriteDocImports}, FSHSP-125), + * avec deux différences : ils importent en plus le bloc ``, et + * leurs blocs de code sont de la prose — on n'y touche pas. + * * Un import peut devoir être SCINDÉ : les symboles d'un même barrel ne vivent * pas forcément dans le même fichier (`{ UiIcon, UiIconType }` → * `ui-icon.ts` + `ui-icon-families.ts`). Une réécriture qui se contenterait de @@ -14,7 +18,7 @@ */ import { dirname, relative } from 'node:path'; import type { AssetUnit } from './component-registry'; -import { resolveSpecifier } from './component-registry'; +import { CONFIG_TABLE_PATH, resolveSpecifier } from './component-registry'; import { buildExportMap } from './export-map'; /** @@ -116,3 +120,72 @@ export function rewriteKitImports(content: string, fileTargetPath: string): Rewr return { content: rewritten, unresolved }; } + +/** `import { ConfigTable } from '../../../../storybook/blocks/config-table';` */ +const CONFIG_TABLE_IMPORT_RE = + /^([ \t]*import\s+(?:type\s+)?\{[^}]*\}\s+from\s+)['"](?:\.\.\/)+storybook\/blocks\/config-table['"](\s*;?[ \t]*)$/gm; + +/** Délimiteur de bloc de code MDX : ``` ou ~~~, éventuellement indenté. */ +const FENCE_RE = /^\s*(```|~~~)/; + +/** + * Applique une réécriture aux seules lignes HORS bloc de code. + * + * Un MDX mêle deux natures de code : ses propres imports ESM, qui doivent + * suivre les copies, et des extraits pédagogiques entre ``` qui montrent au + * lecteur ce qu'il écrira dans SON application. Réécrire les seconds + * produirait des chemins relatifs faux — ils dépendent de l'emplacement du + * fichier du lecteur, que nous ne connaissons pas. + */ +function outsideFencedBlocks(content: string, rewrite: (chunk: string) => string): string { + const lines = content.split('\n'); + const out: string[] = []; + let buffer: string[] = []; + let inFence = false; + + const flush = () => { + if (buffer.length) out.push(rewrite(buffer.join('\n'))); + buffer = []; + }; + + for (const line of lines) { + if (FENCE_RE.test(line)) { + if (!inFence) flush(); + inFence = !inFence; + out.push(line); + continue; + } + if (inFence) out.push(line); + else buffer.push(line); + } + flush(); + + return out.join('\n'); +} + +/** + * Réécrit les imports d'un MDX copié : ceux du kit comme dans un `.ts`, plus + * l'import du bloc `` — qui pointe ici, depuis le kit, vers + * `storybook/blocks/` du monorepo (49 MDX sur 54), et doit désigner chez le + * consommateur la copie que `ng add` y a posée. + * + * Ce que ce passage NE corrige PAS : les extraits de code pédagogiques qui + * importent `@4sh/ui-kit/…` (aujourd'hui `ui-icon.mdx`) restent tels quels — + * voir {@link outsideFencedBlocks}. Le lecteur y verra un package qu'il n'a + * pas ; c'est de la prose à reprendre, pas un import à résoudre. + */ +export function rewriteDocImports(content: string, fileTargetPath: string): RewriteResult { + const unresolved: string[] = []; + + const rewritten = outsideFencedBlocks(content, (chunk) => { + const kit = rewriteKitImports(chunk, fileTargetPath); + unresolved.push(...kit.unresolved); + return kit.content.replace( + CONFIG_TABLE_IMPORT_RE, + (_whole, prefix: string, suffix: string) => + `${prefix}'${relativeSpecifier(fileTargetPath, CONFIG_TABLE_PATH.replace(/\.js$/, ''))}'${suffix}`, + ); + }); + + return { content: rewritten, unresolved }; +} diff --git a/scripts/docs.config.mjs b/scripts/docs.config.mjs index 8419782..a3cd7e0 100644 --- a/scripts/docs.config.mjs +++ b/scripts/docs.config.mjs @@ -40,13 +40,32 @@ import { dirname, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); -const SHARED_CONFIG = join(ROOT, 'projects/ui-kit/styles/settings/_ui-config.scss'); -const COMPONENTS_DIRS = [ - join(ROOT, 'src/app/shared/components'), - // Components already migrated to the `ui-kit` npm package (`ng-packagr` secondary - // entry points) — their `.scss` still documents the same public contract. - join(ROOT, 'projects/ui-kit'), -]; + +/** + * Le script est embarqué tel quel dans `@4sh/ui-kit-schematics` et tourne donc + * dans deux dispositions (FSHSP-125) : ce monorepo, où le kit vit sous + * `projects/ui-kit/`, et un projet consommateur, où il n'existe que des copies + * sous `src/`. Ce sont les mêmes `.scss` et le même contrat `///` : seuls les + * chemins changent. On les déduit du disque plutôt que de maintenir deux + * versions du script, ou de réécrire ses constantes à la copie. + */ +const IS_KIT_MONOREPO = existsSync(join(ROOT, 'projects/ui-kit/styles/settings/_ui-config.scss')); + +const SHARED_CONFIG = IS_KIT_MONOREPO + ? join(ROOT, 'projects/ui-kit/styles/settings/_ui-config.scss') + : // Chez le consommateur, la fondation de styles est posée par `ng add`. + join(ROOT, 'src/styles/ui-kit/settings/_ui-config.scss'); + +const COMPONENTS_DIRS = IS_KIT_MONOREPO + ? [ + join(ROOT, 'src/app/shared/components'), + // Components already migrated to the `ui-kit` npm package (`ng-packagr` secondary + // entry points) — their `.scss` still documents the same public contract. + join(ROOT, 'projects/ui-kit'), + ] + : // Les composants copiés (`components/ui/`) et ceux du projet cohabitent ici. + [join(ROOT, 'src/app/shared/components')]; + const OUT_FILE = join(ROOT, 'storybook/generated/ui-config.json'); // Sections de `_ui-config.scss` → page de doc du groupe partagé. @@ -362,6 +381,9 @@ function resolveBinding(rawValue, { shared, local }) { // --- Collecte --------------------------------------------------------- function walk(dir, out = []) { + // Un projet consommateur peut n'avoir encore copié aucun composant : le + // dossier n'existe pas, ce n'est pas une erreur (la doc est alors vide). + if (!existsSync(dir)) return out; for (const entry of readdirSync(dir, { withFileTypes: true })) { const full = join(dir, entry.name); if (entry.isDirectory()) walk(full, out); @@ -371,6 +393,15 @@ function walk(dir, out = []) { } function parseSharedConfig() { + if (!existsSync(SHARED_CONFIG)) { + // Chez le consommateur, ce fichier arrive avec la fondation de styles. Sans + // lui, chaque composant perdrait ses constantes partagées sans qu'on sache + // dire pourquoi : on nomme la cause plutôt que de produire une doc amputée. + throw new Error( + `${relative(ROOT, SHARED_CONFIG)} introuvable — pose la fondation de styles ` + + `(\`ng add @4sh/ui-kit-schematics\`) avant de générer la doc.`, + ); + } const text = readFileSync(SHARED_CONFIG, 'utf8'); const map = new Map(); for (const decl of parseDeclarations(text)) { diff --git a/scripts/schematics-assets.build.mjs b/scripts/schematics-assets.build.mjs index 99c9fc1..64ac56d 100644 --- a/scripts/schematics-assets.build.mjs +++ b/scripts/schematics-assets.build.mjs @@ -14,8 +14,11 @@ * `ng-package.json`) → `assets/components/{catégorie}/ui-{nom}/` * - une base PARTAGÉE (transverse à une catégorie, ex. `forms/src` qui * porte `BaseFormField`) → `assets/shared/{catégorie}/` - * Seuls `*.ts`, `*.html`, `*.scss` sont copiés — jamais `*.stories.ts`, - * `*.spec.ts`, `*.mdx` (contrat repris de `components.check.mjs`). + * Seuls `*.ts`, `*.html`, `*.scss`, `*.mdx` sont copiés — jamais `*.spec.ts`. + * La story et le MDX en font partie depuis FSHSP-125 : ils sont la doc du + * composant, et un composant copié sans sa doc laisse le consommateur avec un + * Storybook hébergé qui décrit NOS composants, pas ses copies éditées. C'est + * `add --with-storybook` qui décide de les poser ou non chez lui, pas ce script. * * La fondation de styles (`styles/base`, `styles/utils`, `styles/settings`, * `styles/generated`) part elle aussi dans `assets/styles/`, pour `ng-add`. @@ -31,8 +34,10 @@ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const KIT = join(ROOT, 'projects/ui-kit'); const ASSETS = join(ROOT, 'projects/ui-kit-schematics/assets'); -const SOURCE_EXTENSIONS = new Set(['.ts', '.html', '.scss']); -const EXCLUDED_SUFFIXES = ['.stories.ts', '.spec.ts', '.mdx']; +const SOURCE_EXTENSIONS = new Set(['.ts', '.html', '.scss', '.mdx']); +// Les tests restent au kit : ils s'appuient sur son harnais, pas sur celui du +// consommateur, chez qui ils échoueraient sans rien lui apprendre. +const EXCLUDED_SUFFIXES = ['.spec.ts']; function isSourceFile(name) { const ext = name.slice(name.lastIndexOf('.')); @@ -40,7 +45,7 @@ function isSourceFile(name) { return !EXCLUDED_SUFFIXES.some((suffix) => name.endsWith(suffix)); } -/** Copie récursivement les fichiers source (.ts/.html/.scss) d'un dossier vers un autre. */ +/** Copie récursivement les fichiers source (.ts/.html/.scss/.mdx) d'un dossier vers un autre. */ function copySourceTree(srcDir, destDir) { return copyTree(srcDir, destDir, isSourceFile); } @@ -172,10 +177,29 @@ function main() { tokenFiles++; } + // Chaîne de doc (FSHSP-125) — pendant exact de la chaîne de tokens, pour la + // même raison : la section « Theming » de chaque MDX n'est pas écrite à la + // main, elle est lue dans `ui-config.json` que `docs.config.mjs` extrait des + // `///` des `.scss`. Sans le script chez le consommateur, ses tables gèlent + // sur NOS valeurs à la première copie — l'inverse de ce que la doc promet. + // `docs.config.mjs` est copié tel quel : il détecte lui-même sa disposition + // (monorepo ou projet consommateur), aucun chemin à réécrire ici. + let docFiles = 0; + const docsDest = join(ASSETS, 'docs-pipeline'); + for (const [src, name] of [ + [join(ROOT, 'scripts/docs.config.mjs'), 'docs.config.mjs'], + [join(ROOT, 'storybook/blocks/config-table.js'), 'config-table.js'], + ]) { + if (!existsSync(src)) continue; + mkdirSync(docsDest, { recursive: true }); + copyFileSync(src, join(docsDest, name)); + docFiles++; + } + console.log( `[schematics-assets] ${componentCount} composant(s), ${sharedCount} base(s) partagée(s), ` + - `${styleFiles} fichier(s) de style, ${tokenFiles} fichier(s) de pipeline de tokens ` + - `→ ${relative(ROOT, ASSETS)}`, + `${styleFiles} fichier(s) de style, ${tokenFiles} fichier(s) de pipeline de tokens, ` + + `${docFiles} fichier(s) de chaîne de doc → ${relative(ROOT, ASSETS)}`, ); } From a89b5b2b04ed861ae4d8dae942ced880cd09e4b9 Mon Sep 17 00:00:00 2001 From: LBU Date: Tue, 18 Aug 2026 08:35:29 +0200 Subject: [PATCH 2/3] FSHSP-125 feat(schematics): scaffold a working Storybook in the consumer project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La phase 1 posait la doc des composants sans de quoi la lire. `ng add --with-storybook` écrit maintenant la configuration : `storybook/` (main, preview, thème du manager, sélecteur de marque, pages Foundations, Spécifications et Configuration), les cibles `angular.json`, les devDependencies et les scripts npm. Un `npm run storybook` suffit ensuite. Deux natures de fichier, deux provenances. Ce qui vaut tel quel des deux côtés est copié depuis ce dépôt via les assets, donc reste en phase sans recopie manuelle ; ce qui doit diverger — globs, couplages de la preview, marque du manager, tsconfig — est un scaffold écrit pour le consommateur. `restore-component-metadata.ts` ne part pas : il répare les annotations que le linker retire du package compilé, et là-bas les stories visent des sources. Le bloc `ui-image` de la preview n'est écrit que si le composant a été copié, sinon la preview entière tomberait sur un import mort. Les liens `parameters.design` vers notre Figma sont retirés à la copie (décision produit) : le consommateur n'y a pas accès, l'onglet Design n'ouvrirait qu'une porte fermée. Quatre pièges levés en montant un vrai projet Angular 22 de zéro, tous invisibles sur un arbre virtuel : - le CLI reformate à Prettier ce qu'un schematic écrit, et `{/* … */}` en ressort en `{/_ … _/}` : l'en-tête de traçabilité des MDX passe par un `export const`, qui traverse ce formatage sans dommage ; - `@storybook/angular` réclame des peers qu'un projet moderne n'a plus (`build-angular`, `platform-browser-dynamic`, `animations`) — sans elles au package.json, npm résout un majeur antérieur et l'install échoue ; - le builder Storybook valide les assets du `browserTarget` contre son schéma, qui exige `output` — `ng new` ne l'écrit plus ; - `Colors.mdx` importe `tokens.manifest.json` : les chemins du monorepo sont réadressés, et `tokens:build` est chaîné avant le build plutôt que laissé au `postinstall`. Vérifié de bout en bout : `ng new` puis un seul `ng add --with-storybook --all`, `npm run build-storybook` réussi, 579 stories et 64 pages indexées, tables Theming résolues sur les SCSS du projet et API renseignée par Compodoc. --- CHANGELOG.md | 8 +- projects/ui-kit-schematics/README.fr.md | 39 +++- projects/ui-kit-schematics/README.md | 31 ++- .../src/ng-add/files/storybook/main.js | 31 +++ .../src/ng-add/files/storybook/myTheme.ts | 56 +++++ .../src/ng-add/files/storybook/preview.ts | 83 +++++++ .../ng-add/files/storybook/tsconfig.doc.json | 8 + .../src/ng-add/files/storybook/tsconfig.json | 14 ++ .../ui-kit-schematics/src/ng-add/index.ts | 210 ++++++++++++++++++ projects/ui-kit-schematics/src/utils/copy.ts | 12 +- .../ui-kit-schematics/src/utils/kit-paths.ts | 25 +++ .../src/utils/strip-figma.ts | 65 ++++++ projects/ui-kit-schematics/tsconfig.json | 6 +- scripts/schematics-assets.build.mjs | 29 +++ scripts/schematics-package.build.mjs | 8 +- 15 files changed, 596 insertions(+), 29 deletions(-) create mode 100644 projects/ui-kit-schematics/src/ng-add/files/storybook/main.js create mode 100644 projects/ui-kit-schematics/src/ng-add/files/storybook/myTheme.ts create mode 100644 projects/ui-kit-schematics/src/ng-add/files/storybook/preview.ts create mode 100644 projects/ui-kit-schematics/src/ng-add/files/storybook/tsconfig.doc.json create mode 100644 projects/ui-kit-schematics/src/ng-add/files/storybook/tsconfig.json create mode 100644 projects/ui-kit-schematics/src/utils/kit-paths.ts create mode 100644 projects/ui-kit-schematics/src/utils/strip-figma.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ed58a8..54ee0db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,11 +17,13 @@ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le pr ## [Unreleased] ### Added -- **Les composants copiés peuvent emporter leur doc** (FSHSP-125) : `ng add @4sh/ui-kit-schematics --with-storybook` copie, à côté de chaque composant, sa story et sa page MDX, et pose la chaîne qui les alimente (`scripts/docs.config.mjs`, bloc ``). Jusqu'ici un projet en sources copiées possédait son code mais aucune doc : il lui restait un Storybook hébergé qui décrit *nos* composants, pas ses copies éditées. +- **Un projet en sources copiées a désormais son propre Storybook** (FSHSP-125) : `ng add @4sh/ui-kit-schematics --with-storybook` copie, à côté de chaque composant, sa story et sa page MDX, pose la configuration Storybook (`storybook/`, cibles `angular.json`, devDependencies, scripts npm) et la chaîne qui alimente la doc (`scripts/docs.config.mjs`, bloc ``). Un `npm run storybook` suffit ensuite. Jusqu'ici le projet possédait son code mais aucune doc : il lui restait un Storybook hébergé qui décrit *nos* composants, pas ses copies éditées. - Les imports suivent les copies, comme pour les sources : la story désigne ses voisins locaux, le MDX désigne le bloc `` posé dans le projet — mais les extraits de code des pages, eux, ne sont pas réécrits (ce sont des exemples destinés au lecteur, pas des imports à résoudre). Les tables *Theming* restent lues sur les `.scss` du projet : elles décrivent ses valeurs, pas les nôtres. + Deux choses font que cette doc appartient au projet. Les tables *Theming* sont lues sur ses propres `.scss` au build, donc elles décrivent ses valeurs, rebranding compris. Et les globs couvrent tout `src/app/shared/components/**` : une story écrite à côté d'un composant maison apparaît sans toucher à la configuration. - Désactivé par défaut : sans Storybook installé, ce sont des fichiers qui importent des packages absents. Le choix est retenu dans `ui-kit.json` et `update` s'y tient. La configuration Storybook elle-même n'est pas encore posée par le schematic. + Les imports suivent les copies, comme pour les sources — mais les extraits de code des pages ne sont pas réécrits : ce sont des exemples destinés au lecteur, pas des imports à résoudre. Les liens `parameters.design` vers notre fichier Figma sont retirés à la copie : le consommateur n'y a pas accès. Les pages transverses embarquées se limitent à *Foundations*, *Spécifications* et *Configuration* ; `Introduction`, `NpmPackage` et `Overview` parlent du kit lui-même et restent ici. + + Désactivé par défaut : sans Storybook, story et MDX sont deux fichiers qui importent des packages absents. Le choix est retenu dans `ui-kit.json` et `update` s'y tient. - **`@4sh/ui-kit-schematics` embarque son `LICENSE` et ses README** (EN + FR). Le package déclarait `Apache-2.0` sans en fournir le texte, alors que la licence (§4a) demande qu'une redistribution en joigne une copie — d'autant plus ici que ce package existe pour que son contenu soit recopié ailleurs. Sa page npmjs, jusque-là vide, redirige maintenant vers `@4sh/ui-kit` : le compagnon ne s'installe jamais directement. ### Fixed diff --git a/projects/ui-kit-schematics/README.fr.md b/projects/ui-kit-schematics/README.fr.md index 64d4786..cda96d4 100644 --- a/projects/ui-kit-schematics/README.fr.md +++ b/projects/ui-kit-schematics/README.fr.md @@ -41,18 +41,33 @@ jamais une fusion automatique. | `ng generate @4sh/ui-kit-schematics:add` | copier d'autres composants (interactif, ou `--components`, ou `--all`) | | `ng generate @4sh/ui-kit-schematics:update` | diff des composants copiés face aux sources publiées | -### Documenter ses copies : `--with-storybook` - -Désactivé par défaut. Activé, chaque composant arrive avec sa story et sa page -MDX, et le projet reçoit la chaîne qui garde leurs tables *Theming* justes : -`scripts/docs.config.mjs` lit les rôles `///` de vos `.scss`, si bien que les -tables décrivent **vos** valeurs — la raison même de copier les sources. Le choix -est retenu dans `ui-kit.json`, et `update` s'y tient. - -Ce que cela ne fait **pas** encore : installer Storybook ni écrire sa -configuration. Pointez votre `storybook/main.js` sur -`src/app/shared/components/**/*.mdx` et `**/*.stories.ts`, et lancez -`npm run docs:config` avant de le démarrer. +### Votre propre Storybook : `--with-storybook` + +Désactivé par défaut. Activé, vous obtenez un Storybook qui tourne, sur les +composants que vous avez copiés : + +```bash +npm run storybook +``` + +Chaque composant arrive avec sa story et sa page MDX, à côté de ses sources. La +configuration atterrit dans `storybook/` — `main.js`, `preview.ts`, le thème du +manager, le sélecteur de marque, et les pages transverses *Foundations*, +*Spécifications* et *Configuration*. Les cibles `storybook` et `build-storybook` +sont ajoutées à `angular.json`, les devDependencies au `package.json`. + +Deux choses font que cette doc est **la vôtre**, et non une photo de la nôtre. +Les tables *Theming* sont lues sur vos propres `.scss` au build +(`scripts/docs.config.mjs` en extrait les rôles `///`) : elles décrivent vos +valeurs, rebranding compris. Et les globs couvrent tout +`src/app/shared/components/**` : une story écrite à côté de votre propre +composant apparaît sans toucher à la configuration. + +Le choix est retenu dans `ui-kit.json`, et `update` s'y tient. + +Non repris : les liens `parameters.design` vers notre fichier Figma — vous ne +pouvez pas l'ouvrir, ils sont retirés à la copie. Remettez votre `node-id` si +vous en avez un. ### `@4sh/ui-kit` n'est délibérément **pas** installé diff --git a/projects/ui-kit-schematics/README.md b/projects/ui-kit-schematics/README.md index 6d2a818..b4970b6 100644 --- a/projects/ui-kit-schematics/README.md +++ b/projects/ui-kit-schematics/README.md @@ -39,17 +39,32 @@ against newer sources — never an automatic merge. | `ng generate @4sh/ui-kit-schematics:add` | copy more components (interactive, or `--components`, or `--all`) | | `ng generate @4sh/ui-kit-schematics:update` | diff copied components against the published sources | -### Documenting your copies: `--with-storybook` +### Your own Storybook: `--with-storybook` + +Off by default. Turned on, you get a working Storybook of the components you +copied: + +```bash +npm run storybook +``` + +Each component arrives with its story and its MDX page, next to its sources. The +config lands in `storybook/` — `main.js`, `preview.ts`, the manager theme, the +brand switcher, and the shared *Foundations* / *Specifications* / *Configuration* +pages. The `storybook` and `build-storybook` targets are added to `angular.json`, +the devDependencies to `package.json`. + +Two things make the doc *yours* rather than a snapshot of ours. The *Theming* +tables are read off your own `.scss` at build time (`scripts/docs.config.mjs` +collects the `///` roles), so they describe your values, rebranding included. And +the globs cover `src/app/shared/components/**` whole: a story you write next to +your own component shows up with no config change. -Off by default. Turned on, each component arrives with its story and its MDX page, -and the project gets the chain that keeps their *Theming* tables truthful: -`scripts/docs.config.mjs` reads the `///` roles out of your `.scss` files, so the -tables describe **your** values — the point of copying sources in the first place. The choice is recorded in `ui-kit.json`, and `update` honours it. -What this does **not** do yet: install Storybook or write its configuration. Point -your own `storybook/main.js` at `src/app/shared/components/**/*.mdx` and -`**/*.stories.ts`, and run `npm run docs:config` before starting it. +Not carried over: the `parameters.design` links to our Figma file — you cannot +open it, so it is stripped at copy time. Put your own `node-id` back if you have +one. ### `@4sh/ui-kit` is deliberately **not** installed diff --git a/projects/ui-kit-schematics/src/ng-add/files/storybook/main.js b/projects/ui-kit-schematics/src/ng-add/files/storybook/main.js new file mode 100644 index 0000000..7f09524 --- /dev/null +++ b/projects/ui-kit-schematics/src/ng-add/files/storybook/main.js @@ -0,0 +1,31 @@ +/** + * Configuration Storybook posée par `ng add @4sh/ui-kit-schematics + * --with-storybook`. Fichier à vous : modifiez-le librement. + * + * Les globs couvrent `components/` en entier — les composants copiés du kit + * comme les vôtres : une story écrite à côté de votre propre composant est + * ramassée sans rien changer ici. + */ +module.exports = { + stories: [ + './docs/**/*.mdx', + '../src/app/shared/components/**/*.mdx', + '../src/app/shared/components/**/*.stories.@(js|jsx|mjs|ts|tsx)', + ], + addons: ['@storybook/addon-docs', '@storybook/addon-a11y', '@storybook-community/storybook-dark-mode'], + framework: { + name: '@storybook/angular', + options: {}, + }, + webpackFinal: async (config) => { + // Angular définit déjà `process.env.NODE_ENV` ; la garder ici la déclare + // deux fois et webpack échoue sur le conflit. + const definePlugin = config.plugins.find( + (p) => p.constructor.name === 'DefinePlugin' && p.definitions['process.env.NODE_ENV'], + ); + if (definePlugin) { + delete definePlugin.definitions['process.env.NODE_ENV']; + } + return config; + }, +}; diff --git a/projects/ui-kit-schematics/src/ng-add/files/storybook/myTheme.ts b/projects/ui-kit-schematics/src/ng-add/files/storybook/myTheme.ts new file mode 100644 index 0000000..e5ed48f --- /dev/null +++ b/projects/ui-kit-schematics/src/ng-add/files/storybook/myTheme.ts @@ -0,0 +1,56 @@ +/** + * Thème du manager Storybook (barre latérale, barre d'outils) posé par + * `ng add @4sh/ui-kit-schematics --with-storybook`. Fichier à vous. + * + * Ce sont les couleurs du CHÂSSIS Storybook, pas celles de vos composants : + * elles ne peuvent pas venir des design tokens, le manager étant rendu hors de + * votre application. Mettez-y votre marque — et `brandImage` pour votre logo. + */ +import { create } from 'storybook/theming'; + +const brandContent = { + brandTitle: 'Design System', + brandTarget: '_self', + fontBase: '"Inter", sans-serif', + fontCode: 'monospace', + appBorderRadius: 8, + inputBorderRadius: 4, +}; + +export const lightTheme = create({ + base: 'light', + ...brandContent, + appBg: '#f6f7f9', + appContentBg: '#ffffff', + appBorderColor: '#dfe2e7', + textColor: '#111827', + textInverseColor: '#ffffff', + barTextColor: '#6b7280', + barSelectedColor: '#9747ff', + barBg: '#ffffff', + inputBg: '#ffffff', + inputBorder: '#dfe2e7', + inputTextColor: '#111827', + colorPrimary: '#9747ff', + colorSecondary: '#9747ff', +}); + +export const darkTheme = create({ + base: 'dark', + ...brandContent, + appBg: '#0f172a', + appContentBg: '#111827', + appBorderColor: '#1f2937', + textColor: '#f3f4f6', + textInverseColor: '#111827', + barTextColor: '#9ca3af', + barSelectedColor: '#ac78ff', + barBg: '#111827', + inputBg: '#1f2937', + inputBorder: '#374151', + inputTextColor: '#f6f7f9', + colorPrimary: '#9747ff', + colorSecondary: '#9747ff', +}); + +export default lightTheme; diff --git a/projects/ui-kit-schematics/src/ng-add/files/storybook/preview.ts b/projects/ui-kit-schematics/src/ng-add/files/storybook/preview.ts new file mode 100644 index 0000000..6a38409 --- /dev/null +++ b/projects/ui-kit-schematics/src/ng-add/files/storybook/preview.ts @@ -0,0 +1,83 @@ +/** + * Preview Storybook posée par `ng add @4sh/ui-kit-schematics --with-storybook`. + * Fichier à vous : modifiez-le librement. + * + * Il fait trois choses que les stories du kit supposent : brancher le thème + * clair/sombre sur `data-theme` (comme `ThemeService`), offrir le sélecteur de + * marque de la barre d'outils (comme `BrandService`), et fournir les providers + * Angular dont les stories ont besoin. + */ +import { applicationConfig, Preview } from '@storybook/angular'; +import { setCompodocJson } from '@storybook/addon-docs/angular'; +import { provideRouter } from '@angular/router'; +import { addons } from 'storybook/preview-api'; +import { lightTheme, darkTheme } from './myTheme'; +import { brandGlobalTypes, withBrand, DEFAULT_BRAND } from './brand-toolbar'; +// Descriptions des inputs dans l'onglet API : produites par Compodoc, que le +// builder `@storybook/angular` lance lui-même (`compodoc: true` dans angular.json). +import docJson from '../documentation.json'; +// +import { provideUiImageAssets, UiImageAssetsMap } from '../src/app/shared/components/ui/base/ui-image/ui-image'; +// `ui-image` lit ses images locales dans une map injectée : le composant ne peut +// pas deviner l'arborescence d'assets d'un projet. Remplissez +// `src/assets/assets-map.json` pour que ses stories affichent vos visuels. +import assetsMap from '../src/assets/assets-map.json'; +// + +setCompodocJson(docJson); + +/** Applique le mode sombre comme le fait `ThemeService` : un attribut sur . */ +const syncTheme = (isDark: boolean) => { + if (typeof document === 'undefined') return; + const root = document.documentElement; + if (isDark) root.setAttribute('data-theme', 'dark'); + else root.removeAttribute('data-theme'); +}; + +const channel = addons.getChannel(); +channel.on('DARK_MODE', (isDark) => { + setTimeout(() => syncTheme(isDark), 0); +}); + +const preview: Preview = { + initialGlobals: { brand: DEFAULT_BRAND }, + globalTypes: brandGlobalTypes, + decorators: [ + withBrand, + (Story, context) => { + syncTheme(context.globals['darkMode']); + return Story(); + }, + applicationConfig({ + providers: [ + // `provideRouter` : plusieurs composants du kit ont des liens + // (`ui-link`, `ui-breadcrumb`, `ui-menu`…) qui exigent un Router. + // Pas de `provideAnimations` ici : le kit anime en CSS (`ui-motion`). + // Le package `@angular/animations` est installé (le renderer Storybook + // l'exige) : si VOS composants animent avec Angular, ajoutez le + // provider, rien d'autre à installer. + provideRouter([]), + // + provideUiImageAssets(assetsMap as UiImageAssetsMap), + // + ], + }), + ], + parameters: { + layout: 'centered', + docs: { + story: { inline: true }, + }, + darkMode: { + dark: darkTheme, + light: lightTheme, + stylePreview: true, + classTarget: 'html', + darkClass: 'dark-mode', + lightClass: 'light-mode', + }, + backgrounds: { disable: true }, + }, +}; + +export default preview; diff --git a/projects/ui-kit-schematics/src/ng-add/files/storybook/tsconfig.doc.json b/projects/ui-kit-schematics/src/ng-add/files/storybook/tsconfig.doc.json new file mode 100644 index 0000000..69921a3 --- /dev/null +++ b/projects/ui-kit-schematics/src/ng-add/files/storybook/tsconfig.doc.json @@ -0,0 +1,8 @@ +/* tsconfig lu par Compodoc, qui produit les descriptions de l'onglet API. + Les stories en sont exclues : ce sont des exemples, pas de l'API. */ +{ + "extends": "./tsconfig.json", + "exclude": ["../src/**/*.spec.ts", "../src/**/*.stories.ts"], + "include": ["../src/**/*"], + "files": ["./typings.d.ts"] +} diff --git a/projects/ui-kit-schematics/src/ng-add/files/storybook/tsconfig.json b/projects/ui-kit-schematics/src/ng-add/files/storybook/tsconfig.json new file mode 100644 index 0000000..baec217 --- /dev/null +++ b/projects/ui-kit-schematics/src/ng-add/files/storybook/tsconfig.json @@ -0,0 +1,14 @@ +/* tsconfig de la configuration Storybook (posé par `ng add + @4sh/ui-kit-schematics --with-storybook`). Il étend celui de l'application + pour compiler stories et preview avec les mêmes règles que votre code. */ +{ + "extends": "../tsconfig.app.json", + "compilerOptions": { + "types": ["node"], + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true + }, + "exclude": ["../src/**/*.spec.ts", "../src/main.ts"], + "include": ["../src/**/*", "./preview.ts"], + "files": ["./typings.d.ts"] +} diff --git a/projects/ui-kit-schematics/src/ng-add/index.ts b/projects/ui-kit-schematics/src/ng-add/index.ts index c74d35e..9f07a8b 100644 --- a/projects/ui-kit-schematics/src/ng-add/index.ts +++ b/projects/ui-kit-schematics/src/ng-add/index.ts @@ -20,6 +20,7 @@ import { addDependency, addNpmScript, readPackageJson } from '../utils/package-j import { emptyManifest, MANIFEST_PATH, writeManifest } from '../utils/manifest'; import { CONFIG_TABLE_PATH, docsPipelineDir, stylesFoundationDir } from '../utils/component-registry'; import { readKitManifestInfo } from '../utils/kit-manifest'; +import { rewriteKitPaths } from '../utils/kit-paths'; import { add } from '../add'; /** Copie la fondation de styles selon l'arborescence validée avec le designer @@ -172,6 +173,205 @@ function copyDocsPipeline(): Rule { }; } +/** Chemin de la copie de `ui-image` : la preview ne câble ses providers que s'il est là. */ +const UI_IMAGE_PATH = 'src/app/shared/components/ui/base/ui-image/ui-image.ts'; + +/** Bornes du bloc conditionnel de `preview.ts` (voir {@link scaffoldStorybook}). */ +const UI_IMAGE_BLOCK_RE = /^[ \t]*\/\/ \n[\s\S]*?^[ \t]*\/\/ <\/ui-image>\n/gm; +const UI_IMAGE_MARKER_RE = /^[ \t]*\/\/ <\/?ui-image>\n/gm; + +/** + * Pose la configuration Storybook (FSHSP-125). + * + * Deux natures de fichier, deux provenances : ce qui vaut tel quel ici comme + * là-bas (`manager.ts`, `brand-toolbar.ts`, `preview-head.html`, les pages de + * doc transverses) vient des assets, synchronisé depuis ce dépôt ; ce qui doit + * diverger (`main.js` et ses globs, `preview.ts` et ses couplages, `myTheme.ts` + * et sa marque, les tsconfig) est un scaffold écrit pour le consommateur. + * + * La règle tourne APRÈS la copie des composants, et pas avant : c'est l'arbre + * qui dit si `ui-image` en fait partie. Sa preview a besoin de providers que + * lui seul utilise — les écrire sans lui casserait tout le Storybook sur un + * import mort. + */ +function scaffoldStorybook(): Rule { + return (tree: Tree, context: SchematicContext) => { + const assets = join(stylesFoundationDir(), '..', 'storybook'); + const scaffolds = join(__dirname, 'files', 'storybook'); + + const create = (targetPath: string, content: string) => { + if (tree.exists(targetPath)) return; // fichier du consommateur dès la première pose + tree.create(targetPath, content); + }; + + const copyAll = (srcDir: string, targetDir: string) => { + for (const entry of readdirSync(srcDir, { withFileTypes: true })) { + const full = join(srcDir, entry.name); + if (entry.isDirectory()) { + copyAll(full, `${targetDir}/${entry.name}`); + continue; + } + const source = readFileSync(full, 'utf8'); + // Les pages transverses citent la fondation de styles — `Colors.mdx` + // va jusqu'à importer le manifeste de tokens. Aux chemins du monorepo, + // le build échoue sur un module introuvable. + create(`${targetDir}/${entry.name}`, entry.name.endsWith('.mdx') ? rewriteKitPaths(source) : source); + } + }; + + copyAll(assets, 'storybook'); + + for (const name of ['main.js', 'myTheme.ts', 'tsconfig.json', 'tsconfig.doc.json']) { + create(`storybook/${name}`, readFileSync(join(scaffolds, name), 'utf8')); + } + + const hasUiImage = tree.exists(UI_IMAGE_PATH); + const preview = readFileSync(join(scaffolds, 'preview.ts'), 'utf8'); + create('storybook/preview.ts', hasUiImage ? preview.replace(UI_IMAGE_MARKER_RE, '') : preview.replace(UI_IMAGE_BLOCK_RE, '')); + + // `ui-image` résout ses images dans cette map. Vide, ses stories affichent + // le placeholder plutôt que de faire échouer la compilation de la preview. + if (hasUiImage) create('src/assets/assets-map.json', '{}\n'); + + context.logger.info('✔ Configuration Storybook posée (storybook/).'); + return tree; + }; +} + +/** + * Cibles `storybook` / `build-storybook`, calquées sur les options de `build` + * du projet — mêmes feuilles de style et mêmes `includePaths`, sans quoi les + * `@use 'utils'` des composants copiés ne résolvent pas dans la preview. + * + * `compodoc: true` : le builder lance Compodoc lui-même avant de démarrer, et + * c'est ce `documentation.json` qui donne aux `ArgTypes` leurs descriptions. + */ +function addStorybookTargets(): Rule { + return updateWorkspace((workspace) => { + for (const [name, project] of workspace.projects) { + const build = project.targets.get('build'); + if (!build || project.extensions['projectType'] !== 'application') continue; + if (project.targets.has('storybook')) continue; // déjà câblé : on ne réécrit pas + + // Le builder Storybook valide les options du `browserTarget` contre SON + // schéma, plus ancien : il y exige `output` sur chaque motif d'asset, + // qu'`ng new` n'écrit plus depuis que `@angular/build` lui donne `.` + // pour défaut. Sans ce complément, `ng run …:build-storybook` s'arrête + // sur « must have required property 'output' » en désignant un fichier + // que le consommateur n'a pas écrit. La valeur ajoutée est celle que le + // builder d'application applique déjà : le comportement ne change pas. + const assets = build.options?.['assets']; + if (Array.isArray(assets)) { + build.options!['assets'] = assets.map((asset) => + asset && typeof asset === 'object' && !('output' in asset) ? { ...asset, output: '.' } : asset, + ); + } + + const shared = { + configDir: 'storybook', + browserTarget: `${name}:build`, + assets: [{ glob: '**/*', input: 'src/assets', output: './assets/' }], + styles: build.options?.['styles'], + stylePreprocessorOptions: build.options?.['stylePreprocessorOptions'], + compodoc: true, + compodocArgs: ['-e', 'json', '-d', '.'], + }; + + project.targets.add({ + name: 'storybook', + builder: '@storybook/angular:start-storybook', + options: { ...shared, port: 6006 }, + }); + project.targets.add({ + name: 'build-storybook', + builder: '@storybook/angular:build-storybook', + options: { ...shared, outputDir: 'storybook-static' }, + }); + } + }); +} + +/** + * Nom du projet que les scripts npm doivent viser : la première application + * d'`angular.json`, celle à laquelle `addStorybookTargets` a ajouté ses cibles. + */ +function firstApplicationName(tree: Tree): string | null { + const buffer = tree.read('/angular.json'); + if (!buffer) return null; + const workspace = JSON.parse(buffer.toString('utf8')) as { + projects?: Record }>; + }; + for (const [name, project] of Object.entries(workspace.projects ?? {})) { + if (project.projectType === 'application') return name; + } + return null; +} + +/** + * Plage de version d'Angular déclarée par le projet (`^22.1.0`), telle quelle. + * + * Reprise mot pour mot, et non normalisée en `^22.0.0` : les paquets Angular + * s'exigent l'un l'autre à la version EXACTE, donc un `platform-browser-dynamic` + * résolu plus haut que le `@angular/common` déjà verrouillé casse l'install. + * Demander la même plage laisse npm les déduire ensemble. + */ +function angularRange(tree: Tree): string { + return readPackageJson(tree).dependencies?.['@angular/core'] ?? '^22.0.0'; +} + +/** Dépendances et scripts du Storybook du consommateur. Versions alignées sur ce dépôt. */ +function addStorybookDependencies(): Rule { + return (tree: Tree, context: SchematicContext) => { + // Deux peerDependencies de `@storybook/angular` qu'un projet Angular récent + // n'a plus : il bâtit avec `@angular/build`, et ne rend plus en JIT. Sans + // elles au `package.json`, npm les résout seul — il tombe sur le majeur + // précédent, dont les peers entrent en conflit avec l'Angular du projet, et + // l'install s'arrête sur un ERESOLVE qui ne dit pas d'où il vient. On les + // déclare au millésime du projet, comme ce dépôt le fait pour lui-même. + // `@angular/animations` est de la partie bien que le kit ne s'en serve pas : + // le renderer de `@storybook/angular` fait un `import()` dynamique de + // `@angular/platform-browser/animations` pour reconnaître + // `BrowserAnimationsModule`. Absent, le build PASSE — webpack se contente + // d'un stub — et c'est au premier rendu d'une story que tout s'arrête sur + // « Cannot find module ». Le package doit être là, pas son provider. + for (const name of [ + '@angular-devkit/build-angular', + '@angular/platform-browser-dynamic', + '@angular/animations', + ]) { + addDependency(tree, name, angularRange(tree), 'devDependencies'); + } + for (const [name, version] of [ + ['storybook', '^10.5.0'], + ['@storybook/angular', '^10.5.0'], + ['@storybook/addon-docs', '^10.5.0'], + ['@storybook/addon-a11y', '^10.5.0'], + ['@storybook-community/storybook-dark-mode', '^7.1.0'], + // Lancé par le builder (`compodoc: true`), pas par un script à nous. + ['@compodoc/compodoc', '^1.1.26'], + ] as const) { + addDependency(tree, name, version, 'devDependencies'); + } + // Les deux générations d'abord, dans cet ordre : la page « Colors » importe + // `tokens.manifest.json` (produit par `tokens:build`) et le bloc + // `` lit `ui-config.json` (produit par `docs:config`). Les + // chaîner ici plutôt que de compter sur le `postinstall` : sur une + // installation fraîche, le build tomberait sinon sur un module introuvable. + const project = firstApplicationName(tree); + if (project) { + const generate = 'npm run tokens:build && npm run docs:config'; + addNpmScript(tree, 'storybook', `${generate} && ng run ${project}:storybook`); + addNpmScript(tree, 'build-storybook', `${generate} && ng run ${project}:build-storybook`); + } else { + context.logger.warn( + "Aucune application trouvée dans angular.json : scripts npm `storybook` non écrits (les cibles, elles, n'ont pas pu être ajoutées non plus).", + ); + } + context.logger.info('✔ Dépendances et scripts Storybook ajoutés.'); + return tree; + }; +} + function addRuntimeDependencies(): Rule { return (tree: Tree, context: SchematicContext) => { const { peerDependencies } = readKitManifestInfo(); @@ -247,6 +447,14 @@ export function ngAdd(options: Schema): Rule { createManifest(withStorybook), ]; + // Après la fondation ET après la copie : le scaffold lit l'arbre pour savoir + // si `ui-image` est là. Les cibles et les dépendances peuvent, elles, être + // écrites n'importe quand — on les groupe ici pour n'avoir qu'un seul bloc + // Storybook dans la chaîne. + const storybook = withStorybook + ? [scaffoldStorybook(), addStorybookTargets(), addStorybookDependencies()] + : []; + // En queue de chaîne : la tâche s'exécute après application de l'arbre, donc // une fois `package.json` écrit. `--skip-install` la retire, pour un projet qui // pilote son lockfile lui-même (CI, monorepo). @@ -259,6 +467,7 @@ export function ngAdd(options: Schema): Rule { if (options.skipComponents) { return chain([ ...foundation, + ...storybook, (_tree: Tree, context: SchematicContext) => { context.logger.info( 'Fondation posée. Composants à copier ensuite : `ng generate @4sh/ui-kit-schematics:add`.', @@ -271,6 +480,7 @@ export function ngAdd(options: Schema): Rule { return chain([ ...foundation, add({ components: options.components, all: options.all, withStorybook }), + ...storybook, ...install, ]); } diff --git a/projects/ui-kit-schematics/src/utils/copy.ts b/projects/ui-kit-schematics/src/utils/copy.ts index b2419f5..155f138 100644 --- a/projects/ui-kit-schematics/src/utils/copy.ts +++ b/projects/ui-kit-schematics/src/utils/copy.ts @@ -11,14 +11,19 @@ import type { AssetUnit } from './component-registry'; import { flattenedRelPath, isStorybookFile, unitSourceFiles } from './component-registry'; import { BARREL_FILENAME } from './export-map'; import { rewriteDocImports, rewriteKitImports } from './rewrite-imports'; +import { stripFigmaDesign } from './strip-figma'; const HEADER_STYLE: Record string> = { '.ts': (lines) => lines.map((l) => `// ${l}`).join('\n') + '\n', '.scss': (lines) => lines.map((l) => `// ${l}`).join('\n') + '\n', '.html': (lines) => `\n`, - // Commentaire d'expression JSX : le MDX v3 ne connaît plus ``, qu'il - // rendrait tel quel en haut de la page de doc. - '.mdx': (lines) => `{/*\n${lines.map((l) => ` ${l}`).join('\n')}\n*/}\n\n`, + // Une constante exportée, et non le commentaire `{/* … */}` qu'on attendrait + // ici : c'est la seule forme de commentaire que le MDX accepte, et elle ne + // survit pas au passage de Prettier que le CLI Angular applique aux fichiers + // écrits par un schematic (`{/*` en ressort en `{/_`, et l'indexeur de + // Storybook ne parse plus la page). Un `export const` traverse ce formatage + // sans dommage, reste invisible au rendu, et porte la même mention. + '.mdx': (lines) => `export const uiKitOrigin = ${JSON.stringify(lines.join(' '))};\n\n`, }; function traceabilityHeader(unit: AssetUnit, relPath: string, kitVersion: string, ext: string): string { @@ -82,6 +87,7 @@ export function renderUnitFiles(unit: AssetUnit, kitVersion: string, options: Re claimedBy.set(targetPath, absSrc); let source = readFileSync(absSrc, 'utf8'); + if (absSrc.endsWith('.stories.ts')) source = stripFigmaDesign(source); if (ext === '.ts' || ext === '.mdx') { const result = ext === '.mdx' ? rewriteDocImports(source, targetPath) : rewriteKitImports(source, targetPath); source = result.content; diff --git a/projects/ui-kit-schematics/src/utils/kit-paths.ts b/projects/ui-kit-schematics/src/utils/kit-paths.ts new file mode 100644 index 0000000..5e02214 --- /dev/null +++ b/projects/ui-kit-schematics/src/utils/kit-paths.ts @@ -0,0 +1,25 @@ +/** + * kit-paths — réadresse les chemins du monorepo cités par les pages de doc + * transverses (FSHSP-125). + * + * Ces pages sont copiées telles quelles depuis ce dépôt, où la fondation de + * styles vit sous `projects/ui-kit/styles/`. Chez le consommateur elle est + * ailleurs, et la citation n'est pas décorative : `Colors.mdx` IMPORTE le + * manifeste de tokens, et le build s'arrête net sur un module introuvable. + * Les mentions en prose passent par la même table — un chemin faux dans une + * doc coûte au lecteur le temps de le chercher. + */ + +/** Ordre significatif : `base/` sort de `ui-kit/`, il doit être testé avant le préfixe général. */ +const PATH_MAP: readonly (readonly [string, string])[] = [ + ['projects/ui-kit/styles/base/', 'src/styles/base/'], + ['projects/ui-kit/styles/', 'src/styles/ui-kit/'], +]; + +export function rewriteKitPaths(content: string): string { + let out = content; + for (const [from, to] of PATH_MAP) { + out = out.split(from).join(to); + } + return out; +} diff --git a/projects/ui-kit-schematics/src/utils/strip-figma.ts b/projects/ui-kit-schematics/src/utils/strip-figma.ts new file mode 100644 index 0000000..e71da3e --- /dev/null +++ b/projects/ui-kit-schematics/src/utils/strip-figma.ts @@ -0,0 +1,65 @@ +/** + * strip-figma — retire le bloc `design: { type: 'figma', url: … }` des stories + * copiées (FSHSP-125). + * + * 48 des 53 stories du kit pointent, via `parameters.design.url`, vers NOTRE + * fichier Figma. Chez le consommateur, l'onglet « Design » n'ouvrirait qu'un + * lien qu'il ne peut pas suivre : on le retire plutôt que de livrer une porte + * fermée. À lui de rebrancher son propre `node-id` s'il en a un. + * + * La suppression compte les accolades au lieu de faire confiance à une + * expression régulière : une URL Figma porte `?node-id=…&t=…`, et un futur + * bloc pourrait gagner une clé. Un compteur ne se trompe pas de fermeture. + */ + +/** Ligne d'ouverture du bloc, seule sur sa ligne : ` design: {`. */ +const DESIGN_OPEN_RE = /^([ \t]*)design:\s*\{[ \t]*$/; + +/** `parameters: {}` devenu vide après retrait — plus rien à y lire. */ +const EMPTY_PARAMETERS_RE = /^[ \t]*parameters:\s*\{\s*\},?[ \t]*$/; + +/** + * Indice de la ligne qui ferme le bloc ouvert en `start`, ou `-1` si la + * fermeture n'est jamais atteinte (source malformée : on préfère ne rien + * toucher). Les accolades des chaînes de caractères ne comptent pas. + */ +function findBlockEnd(lines: string[], start: number): number { + let depth = 0; + for (let i = start; i < lines.length; i++) { + let inString: string | null = null; + for (const char of lines[i]) { + if (inString) { + if (char === inString) inString = null; + continue; + } + if (char === "'" || char === '"' || char === '`') inString = char; + else if (char === '{') depth++; + else if (char === '}') { + depth--; + if (depth === 0) return i; + } + } + } + return -1; +} + +/** Retire chaque bloc `design: { … }` d'une story, et le `parameters: {}` qu'il laisserait vide. */ +export function stripFigmaDesign(content: string): string { + const lines = content.split('\n'); + const kept: string[] = []; + + for (let i = 0; i < lines.length; i++) { + if (!DESIGN_OPEN_RE.test(lines[i])) { + kept.push(lines[i]); + continue; + } + const end = findBlockEnd(lines, i); + if (end === -1) { + kept.push(lines[i]); + continue; + } + i = end; // les lignes du bloc, fermeture comprise, ne sont pas reprises + } + + return kept.filter((line) => !EMPTY_PARAMETERS_RE.test(line)).join('\n'); +} diff --git a/projects/ui-kit-schematics/tsconfig.json b/projects/ui-kit-schematics/tsconfig.json index 288dd72..a00f410 100644 --- a/projects/ui-kit-schematics/tsconfig.json +++ b/projects/ui-kit-schematics/tsconfig.json @@ -18,5 +18,9 @@ "paths": {} }, "include": ["src/**/*.ts"], - "exclude": ["src/**/*.spec.ts"] + /* `files/` porte des scaffolds destinés au projet consommateur (preview + Storybook, thème du manager…). Ils sont copiés tels quels, jamais + compilés ici : leurs imports (`@storybook/angular`, `../documentation.json`) + n'existent que là-bas. */ + "exclude": ["src/**/*.spec.ts", "src/**/files/**"] } diff --git a/scripts/schematics-assets.build.mjs b/scripts/schematics-assets.build.mjs index 64ac56d..75a91d6 100644 --- a/scripts/schematics-assets.build.mjs +++ b/scripts/schematics-assets.build.mjs @@ -196,6 +196,35 @@ function main() { docFiles++; } + // Configuration Storybook — la part qui vaut TELLE QUELLE chez le + // consommateur, donc copiée d'ici pour rester en phase avec ce dépôt sans + // recopie manuelle. Ce qui doit diverger (`main.js` et ses globs, `preview.ts` + // et ses couplages, `myTheme.ts` et sa marque, les tsconfig) est écrit en + // scaffold dans `src/ng-add/files/storybook/` : deux natures, deux endroits. + // + // `restore-component-metadata.ts` n'est PAS du lot : il répare les + // annotations que le linker retire du package *compilé* que nos stories + // importent. Chez le consommateur les stories visent des sources, compilées + // avec le reste de son app — le décorateur n'aurait rien à réparer. + for (const name of ['manager.ts', 'brand-toolbar.ts', 'preview-head.html', 'typings.d.ts']) { + const src = join(ROOT, 'storybook', name); + if (!existsSync(src)) continue; + mkdirSync(join(ASSETS, 'storybook'), { recursive: true }); + copyFileSync(src, join(ASSETS, 'storybook', name)); + docFiles++; + } + + // Pages de doc transverses. `Introduction`, `NpmPackage` et `Overview` restent + // ici : les deux premières racontent l'installation de `@4sh/ui-kit`, la + // troisième est validée par `components.check.mjs` contre NOTRE inventaire. + // Les pages `config/` sont, elles, la cible des liens `?path=/docs/…` que + // portent les MDX des composants : sans elles, ces liens tombent dans le vide. + for (const group of ['foundations', 'specifications', 'config']) { + const src = join(ROOT, 'storybook/docs', group); + if (!existsSync(src)) continue; + docFiles += copyTree(src, join(ASSETS, 'storybook/docs', group), (n) => n.endsWith('.mdx')); + } + console.log( `[schematics-assets] ${componentCount} composant(s), ${sharedCount} base(s) partagée(s), ` + `${styleFiles} fichier(s) de style, ${tokenFiles} fichier(s) de pipeline de tokens, ` + diff --git a/scripts/schematics-package.build.mjs b/scripts/schematics-package.build.mjs index a80a06f..4ebd9d6 100644 --- a/scripts/schematics-package.build.mjs +++ b/scripts/schematics-package.build.mjs @@ -16,7 +16,7 @@ import { execSync } from 'node:child_process'; import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; -import { dirname, join, resolve } from 'node:path'; +import { dirname, join, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); @@ -56,9 +56,13 @@ for (const name of ['README.md', 'README.fr.md', 'LICENSE']) { if (existsSync(src)) copyFileSync(src, join(DEST, name)); } // `schema.json` n'est pas un `.ts` : tsc ne le copie pas tout seul. +// Les `files/` échappent au filtre : ce sont des scaffolds pour le projet +// consommateur (`preview.ts`, `myTheme.ts`…), exclus de la compilation +// justement parce qu'ils doivent partir tels quels — les filtrer ici les +// ferait disparaître du package sans que rien ne le signale. cpSync(join(PKG, 'src'), join(DEST, 'src'), { recursive: true, - filter: (path) => !path.endsWith('.ts'), + filter: (path) => !path.endsWith('.ts') || path.includes(`${sep}files${sep}`), }); cpSync(join(PKG, 'assets'), join(DEST, 'assets'), { recursive: true }); From 1cb81caea32b59085a008467d3fc4861c3b1c7fe Mon Sep 17 00:00:00 2001 From: LBU Date: Tue, 18 Aug 2026 08:41:13 +0200 Subject: [PATCH 3/3] FSHSP-125 refactor(schematics): drop --components from ng add MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'option faisait double emploi avec l'écran qui s'ouvre deux secondes plus tard : la sélection s'y coche à la barre d'espace, `a` prend tout, `i` inverse. Elle était de surcroît le seul chemin cassé de la commande — le CLI Angular transmettait une chaîne là où le schéma attend un tableau, et `ng add --components=ui-button` s'arrêtait sur « Data path "/components" must be array ». Une option qui n'apporte rien et ne marche pas ne se répare pas, elle s'enlève. `--all` reste, pour l'installation non interactive. `--components` reste sur `ng generate …:add`, où l'usage scripté a du sens. Les deux README décrivent maintenant les touches du prompt, puisque c'est désormais par là que passe une sélection partielle. --- projects/ui-kit-schematics/README.fr.md | 3 ++- projects/ui-kit-schematics/README.md | 4 +++- projects/ui-kit-schematics/src/ng-add/index.ts | 6 +++++- projects/ui-kit-schematics/src/ng-add/schema.d.ts | 2 -- projects/ui-kit-schematics/src/ng-add/schema.json | 5 ----- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/projects/ui-kit-schematics/README.fr.md b/projects/ui-kit-schematics/README.fr.md index cda96d4..686fd88 100644 --- a/projects/ui-kit-schematics/README.fr.md +++ b/projects/ui-kit-schematics/README.fr.md @@ -19,7 +19,8 @@ ng add @4sh/ui-kit-schematics Une seule commande : elle pose la fondation (styles, design tokens, `angular.json`), puis demande quels composants copier et les copie, dépendances -comprises. +comprises. La sélection se fait à la case à cocher — espace pour +choisir, a pour tout, i pour inverser. ``` src/app/shared/ diff --git a/projects/ui-kit-schematics/README.md b/projects/ui-kit-schematics/README.md index b4970b6..31e589f 100644 --- a/projects/ui-kit-schematics/README.md +++ b/projects/ui-kit-schematics/README.md @@ -18,7 +18,9 @@ ng add @4sh/ui-kit-schematics ``` One command: it lays the foundation (styles, design tokens, `angular.json`), then -asks which components to copy and copies them, dependencies included. +asks which components to copy and copies them, dependencies included. The prompt +is a checkbox list — space to pick, a for all, +i to invert. ``` src/app/shared/ diff --git a/projects/ui-kit-schematics/src/ng-add/index.ts b/projects/ui-kit-schematics/src/ng-add/index.ts index 9f07a8b..211aae7 100644 --- a/projects/ui-kit-schematics/src/ng-add/index.ts +++ b/projects/ui-kit-schematics/src/ng-add/index.ts @@ -479,7 +479,11 @@ export function ngAdd(options: Schema): Rule { return chain([ ...foundation, - add({ components: options.components, all: options.all, withStorybook }), + // Pas de `components` ici : `ng add` enchaîne sur le prompt, où l'on coche + // à la barre d'espace (`a` = tout). Une liste en ligne de commande ne + // rendrait pas le service que rend déjà cet écran, deux secondes plus tard. + // Elle reste sur `ng generate …:add`, pour un usage scripté. + add({ all: options.all, withStorybook }), ...storybook, ...install, ]); diff --git a/projects/ui-kit-schematics/src/ng-add/schema.d.ts b/projects/ui-kit-schematics/src/ng-add/schema.d.ts index 34a53e8..f4fd3cb 100644 --- a/projects/ui-kit-schematics/src/ng-add/schema.d.ts +++ b/projects/ui-kit-schematics/src/ng-add/schema.d.ts @@ -1,7 +1,5 @@ export interface Schema { skipInstall?: boolean; - /** Noms explicites (`ui-button`, `ui-select`…) — court-circuite le prompt interactif. */ - components?: string[]; /** Copie tous les composants disponibles, sans prompt. */ all?: boolean; /** Pose la fondation seule, sans copier de composant. */ diff --git a/projects/ui-kit-schematics/src/ng-add/schema.json b/projects/ui-kit-schematics/src/ng-add/schema.json index 3c2ada0..32578b4 100644 --- a/projects/ui-kit-schematics/src/ng-add/schema.json +++ b/projects/ui-kit-schematics/src/ng-add/schema.json @@ -9,11 +9,6 @@ "default": false, "description": "Ne pas lancer `npm install` après avoir modifié package.json." }, - "components": { - "type": "array", - "items": { "type": "string" }, - "description": "Composants à copier (ex. ui-button ui-select). Omis → sélection interactive." - }, "all": { "type": "boolean", "default": false,