From 87dc57d12afc1af108faea0f2d067f3d69d638b1 Mon Sep 17 00:00:00 2001 From: LBU Date: Mon, 17 Aug 2026 16:35:36 +0200 Subject: [PATCH 1/7] FSHSP-121 refactor(schematics): flatten the copied tree and lift shared units out of components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `src/` and `lib/` delimit a library's published surface: copied into a consumer app they enclose nothing and buried every component two levels deep. Both go, along with the `public-api.ts` barrel — a publication surface with no purpose once the file belongs to the consumer. Shared bases landed in `components/ui/{category}/_shared/`, which filed services (`theme.service`) and utilities (`mask-engine`) under `components/`. They move to `src/app/shared/ui-core/{category}/`, keeping their per-domain grouping. Component-local files stay next to their component: they serve it alone. `resolveSpecifier` moves to the registry — the dependency graph and the upcoming import rewrite must agree on that mapping, so it gets one home. --- CHANGELOG.md | 11 +++ .../src/utils/component-registry.ts | 55 ++++++++++- projects/ui-kit-schematics/src/utils/copy.ts | 41 +++++---- .../src/utils/dependency-graph.ts | 10 +- .../ui-kit-schematics/src/utils/export-map.ts | 91 +++++++++++++++++++ 5 files changed, 175 insertions(+), 33 deletions(-) create mode 100644 projects/ui-kit-schematics/src/utils/export-map.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f7ccff..011ba6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,17 @@ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le pr - **`@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. ### Changed +- ⚠️ **L'arborescence copiée est aplatie, et les fichiers transverses sortent de `components/`** (FSHSP-121). La copie reproduisait la structure d'`ng-packagr` (`ui-checkbox/src/lib/ui-checkbox.ts`, plus un barrel `src/public-api.ts`) : `src/` et `lib/` délimitent la surface publiée d'une librairie, et une fois le fichier chez le consommateur ils n'encadrent plus rien — ils enfouissaient chaque composant de deux niveaux. Elles disparaissent, le barrel avec : + + ``` + src/app/shared/ + ├── components/ui/{catégorie}/{ui-nom}/{ui-nom}.ts ← uniquement des composants + └── ui-core/{forms|motion|overlay|theming|types}/ ← directives de base, services, utilitaires, types + ``` + + Les bases partagées atterrissaient dans `components/ui/{catégorie}/_shared/`, c'est-à-dire des services (`theme.service`) et des utilitaires (`mask-engine`) rangés sous `components/`. Elles vivent maintenant dans `ui-core/`, en conservant leur regroupement par domaine. Les fichiers propres à un composant (`date-utils`, `ui-alert.types`, `ui-toast.service`…) restent à côté de lui : ils ne servent qu'à lui. + + **Mise à jour non triviale** pour un projet déjà installé en `0.2.0` : les chemins des fichiers copiés changent tous, donc `update` ne reconnaîtra pas les anciens. Déplacer les fichiers existants avant de lancer la commande, ou repartir d'un `add` propre. - **L'en-tête de traçabilité des fichiers copiés par `ng generate @4sh/ui-kit:add` porte la mention de licence** (`Apache-2.0 — Copyright 2026 4SH.`) en plus de son origine et de sa version. Une fois copié, le fichier vit dans le dépôt du consommateur, où plus rien n'indiquait sous quels termes il est fourni. Conséquence attendue : le premier `ng generate @4sh/ui-kit:update` suivant cette version affiche un diff d'une ligne en tête de chaque fichier installé. ## [0.2.0] - 2026-08-14 diff --git a/projects/ui-kit-schematics/src/utils/component-registry.ts b/projects/ui-kit-schematics/src/utils/component-registry.ts index e5da5ca..c96dfbc 100644 --- a/projects/ui-kit-schematics/src/utils/component-registry.ts +++ b/projects/ui-kit-schematics/src/utils/component-registry.ts @@ -5,7 +5,7 @@ * `components.check.mjs`, même philosophie côté kit publié. */ import { readdirSync, existsSync } from 'node:fs'; -import { join } from 'node:path'; +import { join, relative, sep } from 'node:path'; /** Une unité copiable : un composant `ui-*`, ou une base partagée de catégorie. */ export interface AssetUnit { @@ -20,7 +20,10 @@ export interface AssetUnit { } const ASSETS_ROOT = join(__dirname, '..', '..', 'assets'); -const CONSUMER_ROOT = 'src/app/shared/components/ui'; +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'; function listDirs(dir: string): string[] { if (!existsSync(dir)) return []; @@ -40,7 +43,7 @@ export function listComponents(): AssetUnit[] { category, kind: 'component', dir: join(componentsRoot, category, name), - targetDir: join(CONSUMER_ROOT, category, name), + targetDir: join(COMPONENTS_ROOT, category, name), }); } } @@ -55,8 +58,7 @@ export function listSharedBases(): AssetUnit[] { category, kind: 'shared', dir: join(sharedRoot, category), - // Copiée à côté des composants de sa catégorie, jamais dans un composant précis. - targetDir: join(CONSUMER_ROOT, category, `_shared`), + targetDir: join(CORE_ROOT, category), })); } @@ -64,6 +66,49 @@ export function findUnit(name: string): AssetUnit | undefined { return [...listComponents(), ...listSharedBases()].find((u) => u.name === name); } +/** + * Résout un spécificateur importé vers son unité : `{catégorie}/{ui-nom}` → + * composant (`forms/ui-field`), `{catégorie}` seul → base partagée (`forms`). + * Partagé par le graphe de dépendances et la réécriture des imports, qui + * doivent impérativement s'accorder sur ce mapping. + */ +export function resolveSpecifier(specifier: string): AssetUnit | undefined { + const segments = specifier.split('/'); + const candidateName = segments.length > 1 ? segments[segments.length - 1] : segments[0]; + return findUnit(candidateName); +} + export function stylesFoundationDir(): string { return join(ASSETS_ROOT, 'styles'); } + +/** Tous les fichiers d'une unité, chemins absolus. */ +export function unitSourceFiles(unit: AssetUnit): string[] { + const out: string[] = []; + const walk = (dir: string) => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) walk(full); + else out.push(full); + } + }; + walk(unit.dir); + return out; +} + +/** + * Chemin d'un fichier dans l'unité APRÈS aplatissement, relatif à `targetDir`. + * + * `src/` et `lib/` sont l'échafaudage d'`ng-packagr` : ils délimitent la surface + * publiée d'une librairie. Copiés chez le consommateur ils n'encadrent plus rien + * et ne font qu'enfouir les fichiers de deux niveaux (FSHSP-121). On les retire. + * + * `src/lib/ui-icon.ts` → `ui-icon.ts` ; `lib/base-form-field.ts` → + * `base-form-field.ts` ; `ui-level.ts` (déjà à plat) → inchangé. + */ +export function flattenedRelPath(unit: AssetUnit, absPath: string): string { + const segments = relative(unit.dir, absPath).split(sep); + if (segments[0] === 'src') segments.shift(); + if (segments[0] === 'lib') segments.shift(); + return segments.join('/'); +} diff --git a/projects/ui-kit-schematics/src/utils/copy.ts b/projects/ui-kit-schematics/src/utils/copy.ts index de35beb..318be97 100644 --- a/projects/ui-kit-schematics/src/utils/copy.ts +++ b/projects/ui-kit-schematics/src/utils/copy.ts @@ -1,11 +1,13 @@ /** * copy — recopie une unité (`AssetUnit`) dans l'arbre du projet consommateur, - * avec en-tête de traçabilité (origine + version + licence) sur chaque fichier. + * aplatie (FSHSP-121), avec en-tête de traçabilité (origine + version + + * licence) sur chaque fichier. */ import type { Tree } from '@angular-devkit/schematics'; -import { readFileSync, readdirSync } from 'node:fs'; -import { join, relative } from 'node:path'; +import { readFileSync } from 'node:fs'; import type { AssetUnit } from './component-registry'; +import { flattenedRelPath, unitSourceFiles } from './component-registry'; +import { BARREL_FILENAME } from './export-map'; const HEADER_STYLE: Record string> = { '.ts': (lines) => lines.map((l) => `// ${l}`).join('\n') + '\n', @@ -27,16 +29,6 @@ function traceabilityHeader(unit: AssetUnit, relPath: string, kitVersion: string ]); } -function walk(dir: string): string[] { - const out: string[] = []; - for (const entry of readdirSync(dir, { withFileTypes: true })) { - const full = join(dir, entry.name); - if (entry.isDirectory()) out.push(...walk(full)); - else out.push(full); - } - return out; -} - export interface RenderedFile { targetPath: string; content: string; @@ -45,13 +37,24 @@ export interface RenderedFile { /** 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[] { - return walk(unit.dir).map((absSrc) => { - const relPath = relative(unit.dir, absSrc); + const files: RenderedFile[] = []; + + for (const absSrc of unitSourceFiles(unit)) { + // Le barrel est une surface de publication de librairie : il n'a rien à + // faire chez le consommateur, où les imports désignent les fichiers. + if (absSrc.endsWith(BARREL_FILENAME)) continue; + + const relPath = flattenedRelPath(unit, absSrc); const ext = absSrc.slice(absSrc.lastIndexOf('.')); - const targetPath = join(unit.targetDir, relPath); - const header = traceabilityHeader(unit, relPath, kitVersion, ext); - return { targetPath, content: header + readFileSync(absSrc, 'utf8') }; - }); + const targetPath = `${unit.targetDir}/${relPath}`; + + files.push({ + targetPath, + content: traceabilityHeader(unit, relPath, kitVersion, ext) + readFileSync(absSrc, 'utf8'), + }); + } + + return files; } /** Copie tous les fichiers d'une unité dans l'arbre, avec en-tête de traçabilité. */ diff --git a/projects/ui-kit-schematics/src/utils/dependency-graph.ts b/projects/ui-kit-schematics/src/utils/dependency-graph.ts index 81a63a3..4fed40b 100644 --- a/projects/ui-kit-schematics/src/utils/dependency-graph.ts +++ b/projects/ui-kit-schematics/src/utils/dependency-graph.ts @@ -8,7 +8,7 @@ */ import { readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; -import { type AssetUnit, findUnit } from './component-registry'; +import { type AssetUnit, findUnit, resolveSpecifier } from './component-registry'; const IMPORT_RE = /from\s+['"]@4sh\/ui-kit\/([^'"]+)['"]/g; @@ -37,14 +37,6 @@ function importedSpecifiers(unit: AssetUnit): string[] { return [...specifiers]; } -/** Résout un spécificateur importé (`forms/ui-field`, `forms`) vers son unité. */ -function resolveSpecifier(specifier: string): AssetUnit | undefined { - const segments = specifier.split('/'); - // `{catégorie}/{ui-nom}` → composant ; `{catégorie}` seul → base partagée. - const candidateName = segments.length > 1 ? segments[segments.length - 1] : segments[0]; - return findUnit(candidateName); -} - /** * Clôture transitive des dépendances pour une liste de noms sélectionnés. * Renvoie les unités dans un ordre stable, sélection d'origine en tête. diff --git a/projects/ui-kit-schematics/src/utils/export-map.ts b/projects/ui-kit-schematics/src/utils/export-map.ts new file mode 100644 index 0000000..99fef06 --- /dev/null +++ b/projects/ui-kit-schematics/src/utils/export-map.ts @@ -0,0 +1,91 @@ +/** + * export-map — pour une unité copiable, quel fichier exporte quel symbole. + * + * Nécessaire parce que la copie APLATIT l'unité et supprime son barrel + * (`public-api.ts` est une surface de publication de librairie, sans objet dans + * l'app du consommateur — FSHSP-121). Un import qui passait par le barrel + * (`import { BaseFieldControl } from '@4sh/ui-kit/forms'`) doit donc être + * réadressé vers LE fichier qui porte le symbole : il faut la table. + * + * Les barrels du kit sont uniformément `export * from './lib/x'` — aucun + * renommage, aucun export sélectif — et les déclarations exportées sont toutes + * de forme simple. Une lecture par expression régulière suffit donc ici, sans + * embarquer le compilateur TypeScript dans le paquet de schematics. Si l'une ou + * l'autre de ces hypothèses tombait, `assertBarrelIsTrivial` échoue bruyamment + * plutôt que de produire un import silencieusement faux. + */ +import { readFileSync } from 'node:fs'; +import type { AssetUnit } from './component-registry'; +import { unitSourceFiles, flattenedRelPath } from './component-registry'; + +/** `export class Foo`, `export type Bar`, `export abstract class Baz`… */ +const EXPORT_DECL_RE = + /^export\s+(?:declare\s+)?(?:abstract\s+class|class|interface|type|const|let|var|function|enum)\s+([A-Za-z_$][\w$]*)/gm; + +/** Une ligne de barrel : `export * from './lib/ui-icon';` */ +const BARREL_STAR_RE = /^export\s+\*\s+from\s+['"]([^'"]+)['"]\s*;?\s*$/; + +export const BARREL_FILENAME = 'public-api.ts'; + +/** + * Vérifie qu'un barrel ne contient que des `export * from './…'`. Un + * `export { X as Y }` ou un `export default` invaliderait la table construite + * plus bas : mieux vaut interrompre la copie que livrer des imports faux. + */ +function assertBarrelIsTrivial(unit: AssetUnit, barrelPath: string): void { + const lines = readFileSync(barrelPath, 'utf8').split('\n'); + let inBlockComment = false; + for (const [index, raw] of lines.entries()) { + const line = raw.trim(); + if (inBlockComment) { + if (line.includes('*/')) inBlockComment = false; + continue; + } + if (line.startsWith('/*')) { + if (!line.includes('*/')) inBlockComment = true; + continue; + } + if (!line || line.startsWith('//')) continue; + if (!BARREL_STAR_RE.test(line)) { + throw new Error( + `${unit.name} : le barrel ${BARREL_FILENAME} contient une ligne non triviale ` + + `(ligne ${index + 1} : « ${line} »). La copie aplatie ne sait réadresser que des ` + + `\`export * from './…'\`. Adapter export-map.ts avant de publier.`, + ); + } + } +} + +/** Symboles exportés par un fichier source, dans l'ordre de déclaration. */ +function exportedSymbols(absPath: string): string[] { + const content = readFileSync(absPath, 'utf8'); + return [...content.matchAll(EXPORT_DECL_RE)].map((match) => match[1]); +} + +/** + * Table `symbole → chemin du fichier dans l'unité APRÈS aplatissement` + * (ex. `BaseFieldControl` → `base-form-field`, sans extension : c'est la forme + * qu'attend un import TypeScript). + */ +export function buildExportMap(unit: AssetUnit): Map { + const map = new Map(); + + for (const absPath of unitSourceFiles(unit)) { + if (absPath.endsWith(BARREL_FILENAME)) { + assertBarrelIsTrivial(unit, absPath); + continue; + } + if (!absPath.endsWith('.ts')) continue; + + // Chemin aplati, sans extension — c'est ce qu'un import doit désigner. + const flattened = flattenedRelPath(unit, absPath).replace(/\.ts$/, ''); + for (const symbol of exportedSymbols(absPath)) { + // Premier gagnant : un même symbole ne devrait pas être déclaré deux fois + // dans une unité, et si ça arrive c'est le barrel qui trancherait — on ne + // peut pas faire mieux sans compilateur, donc on reste déterministe. + if (!map.has(symbol)) map.set(symbol, flattened); + } + } + + return map; +} From a5ef082d461e95dc807ed2bee92757d6178392b4 Mon Sep 17 00:00:00 2001 From: LBU Date: Mon, 17 Aug 2026 16:36:30 +0200 Subject: [PATCH 2/7] FSHSP-119 fix(schematics): rewrite copied imports to the local copies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A copied component resolved its dependencies from node_modules/@4sh/ui-kit — the compiled code. Editing the copied ui-icon had no effect on the components using it, which cancels the whole point of copying sources. The 139 `@4sh/ui-kit/*` specifiers are now readdressed to the sibling copies. Resolving through a barrel means finding the file that actually carries the symbol, so an import is SPLIT when its symbols are scattered (`{ UiIcon, UiIconType }` -> ui-icon.ts + ui-icon-families.ts). A symbol absent from the table aborts the copy naming it, rather than emitting a wrong path; same for a non-trivial barrel or an unhandled import form. Also fixes: dead copies nobody imported (yet tracked by `update`), the component shipping twice in the bundle, and application code depending on a devDependency, which broke under `npm ci --omit=dev`. --- CHANGELOG.md | 7 + projects/ui-kit-schematics/src/utils/copy.ts | 30 ++++- .../src/utils/rewrite-imports.ts | 123 ++++++++++++++++++ 3 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 projects/ui-kit-schematics/src/utils/rewrite-imports.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 011ba6e..65d3700 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,13 @@ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le pr ### Added - **`@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 +- **Les sources copiées n'importent plus depuis `node_modules`** (FSHSP-119). Un composant copié résolvait ses dépendances dans `node_modules/@4sh/ui-kit`, c'est-à-dire le code compilé : modifier le `ui-icon` copié n'avait aucun effet sur les composants qui l'utilisent, ce qui annulait la raison d'être du starter. Les 139 imports `@4sh/ui-kit/*` des sources sont désormais réadressés vers les copies voisines au moment de la copie. + + Un import passant par un barrel est résolu jusqu'au fichier qui porte réellement le symbole, et **scindé** si ses symboles sont dispersés (`{ UiIcon, UiIconType }` → `ui-icon.ts` + `ui-icon-families.ts`). Un symbole introuvable interrompt la copie en le nommant, plutôt que d'écrire un chemin faux. + + Effets de bord corrigés au passage : les dépendances copiées n'étaient utilisées par personne (copies mortes, suivies par `update` pour rien), le bundle embarquait le composant deux fois, et du code applicatif dépendait d'une `devDependency` — ce qui cassait en `npm ci --omit=dev`. + ### Changed - ⚠️ **L'arborescence copiée est aplatie, et les fichiers transverses sortent de `components/`** (FSHSP-121). La copie reproduisait la structure d'`ng-packagr` (`ui-checkbox/src/lib/ui-checkbox.ts`, plus un barrel `src/public-api.ts`) : `src/` et `lib/` délimitent la surface publiée d'une librairie, et une fois le fichier chez le consommateur ils n'encadrent plus rien — ils enfouissaient chaque composant de deux niveaux. Elles disparaissent, le barrel avec : diff --git a/projects/ui-kit-schematics/src/utils/copy.ts b/projects/ui-kit-schematics/src/utils/copy.ts index 318be97..5b28110 100644 --- a/projects/ui-kit-schematics/src/utils/copy.ts +++ b/projects/ui-kit-schematics/src/utils/copy.ts @@ -1,13 +1,15 @@ /** * copy — recopie une unité (`AssetUnit`) dans l'arbre du projet consommateur, - * aplatie (FSHSP-121), avec en-tête de traçabilité (origine + version + - * licence) sur chaque fichier. + * aplatie (FSHSP-121), imports réadressés vers les copies voisines (FSHSP-119), + * avec en-tête de traçabilité (origine + version + licence) sur chaque fichier. */ import type { Tree } from '@angular-devkit/schematics'; +import { SchematicsException } from '@angular-devkit/schematics'; import { readFileSync } from 'node:fs'; import type { AssetUnit } from './component-registry'; import { flattenedRelPath, unitSourceFiles } from './component-registry'; import { BARREL_FILENAME } from './export-map'; +import { rewriteKitImports } from './rewrite-imports'; const HEADER_STYLE: Record string> = { '.ts': (lines) => lines.map((l) => `// ${l}`).join('\n') + '\n', @@ -38,6 +40,7 @@ export interface RenderedFile { * sans rien écrire — réutilisé par `copyUnit` et par le diff d'`update`. */ export function renderUnitFiles(unit: AssetUnit, kitVersion: string): RenderedFile[] { const files: RenderedFile[] = []; + const unresolved: string[] = []; for (const absSrc of unitSourceFiles(unit)) { // Le barrel est une surface de publication de librairie : il n'a rien à @@ -48,10 +51,25 @@ export function renderUnitFiles(unit: AssetUnit, kitVersion: string): RenderedFi const ext = absSrc.slice(absSrc.lastIndexOf('.')); const targetPath = `${unit.targetDir}/${relPath}`; - files.push({ - targetPath, - content: traceabilityHeader(unit, relPath, kitVersion, ext) + readFileSync(absSrc, 'utf8'), - }); + let source = readFileSync(absSrc, 'utf8'); + if (ext === '.ts') { + const result = rewriteKitImports(source, targetPath); + source = result.content; + unresolved.push(...result.unresolved.map((item) => `${relPath} → ${item}`)); + } + + files.push({ targetPath, content: traceabilityHeader(unit, relPath, kitVersion, ext) + source }); + } + + if (unresolved.length) { + // Laisser passer produirait un projet qui ne compile pas, avec une cause + // très difficile à remonter jusqu'ici. On échoue sur place, en nommant. + throw new SchematicsException( + `${unit.name} : ${unresolved.length} import(s) du kit non réadressé(s) —\n ` + + unresolved.join('\n ') + + `\nCorriger la table d'exports (utils/export-map.ts) ou le mapping ` + + `(utils/component-registry.ts#resolveSpecifier) avant de publier.`, + ); } return files; diff --git a/projects/ui-kit-schematics/src/utils/rewrite-imports.ts b/projects/ui-kit-schematics/src/utils/rewrite-imports.ts new file mode 100644 index 0000000..4aa710a --- /dev/null +++ b/projects/ui-kit-schematics/src/utils/rewrite-imports.ts @@ -0,0 +1,123 @@ +/** + * rewrite-imports — transforme les imports `@4sh/ui-kit/…` des fichiers copiés + * en chemins relatifs vers les copies voisines (FSHSP-119). + * + * Sans cette passe, un composant copié continue de résoudre ses dépendances + * dans `node_modules/@4sh/ui-kit` : modifier le `ui-icon` copié n'aurait alors + * 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. + * + * 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 + * remplacer le spécificateur produirait un import faux. + */ +import { dirname, relative } from 'node:path'; +import type { AssetUnit } from './component-registry'; +import { resolveSpecifier } from './component-registry'; +import { buildExportMap } from './export-map'; + +/** + * `import { A, B } from '@4sh/ui-kit/forms';` — capture la liste de symboles et + * le spécificateur. Volontairement limité aux imports nommés : le kit n'expose + * ni export par défaut ni import de namespace, et un `import *` doit échouer + * bruyamment plutôt que d'être réécrit de travers. + */ +const NAMED_IMPORT_RE = /^[ \t]*import\s+(type\s+)?\{([^}]*)\}\s+from\s+['"]@4sh\/ui-kit\/([^'"]+)['"]\s*;?[ \t]*$/gm; + +/** Toute autre forme d'import du kit — non gérée, donc signalée. */ +const ANY_KIT_IMPORT_RE = /^[ \t]*import\s+(?!(?:type\s+)?\{)[^;\n]*['"]@4sh\/ui-kit(?:\/[^'"]*)?['"]/gm; + +/** Cache par unité : `buildExportMap` relit tous les fichiers de l'unité, et une + * même unité est consultée par chaque fichier qui l'importe. */ +const exportMapCache = new Map>(); + +function exportMapFor(unit: AssetUnit): Map { + let map = exportMapCache.get(unit.name); + if (!map) { + map = buildExportMap(unit); + exportMapCache.set(unit.name, map); + } + return map; +} + +/** `../../base/ui-icon/ui-icon` — toujours préfixé `./` ou `../`, jamais nu. */ +function relativeSpecifier(fromFileTargetPath: string, toTargetPath: string): string { + const rel = relative(dirname(fromFileTargetPath), toTargetPath).split(/[\\/]/).join('/'); + return rel.startsWith('.') ? rel : `./${rel}`; +} + +/** Un symbole importé, avec son éventuel alias : `UiIcon`, `UiIcon as Icon`. */ +function symbolName(clause: string): string { + return clause.trim().split(/\s+as\s+/)[0].trim(); +} + +export interface RewriteResult { + content: string; + /** Spécificateurs rencontrés qu'aucune unité ne porte (à remonter à l'appelant). */ + unresolved: string[]; +} + +/** + * Réécrit les imports du kit dans le contenu d'un fichier déjà copié. + * + * @param content contenu source du fichier + * @param fileTargetPath chemin du fichier CHEZ LE CONSOMMATEUR (c'est depuis lui + * que les chemins relatifs sont calculés, pas depuis la source) + */ +export function rewriteKitImports(content: string, fileTargetPath: string): RewriteResult { + const unresolved: string[] = []; + + const rewritten = content.replace( + NAMED_IMPORT_RE, + (whole, typeOnly: string | undefined, clauses: string, specifier: string) => { + const unit = resolveSpecifier(specifier); + if (!unit) { + unresolved.push(specifier); + return whole; // laissé tel quel : l'appelant décide quoi en faire + } + + const map = exportMapFor(unit); + const prefix = `import ${typeOnly ? 'type ' : ''}`; + + // Regroupe les symboles par fichier de destination : c'est ce qui permet + // de scinder un import dont les symboles sont dispersés. + const byFile = new Map(); + for (const clause of clauses.split(',')) { + const trimmed = clause.trim(); + if (!trimmed) continue; + const target = map.get(symbolName(trimmed)); + if (!target) { + // Symbole absent de la table : on ne devine pas. L'unité est signalée + // comme non résolue et l'import d'origine est conservé, ce qui casse + // au build du consommateur — bien plus lisible qu'un chemin inventé. + unresolved.push(`${specifier} (symbole ${symbolName(trimmed)})`); + return whole; + } + const list = byFile.get(target); + if (list) list.push(trimmed); + else byFile.set(target, [trimmed]); + } + + return [...byFile.entries()] + .map(([file, symbols]) => { + const to = `${unit.targetDir}/${file}`; + return `${prefix}{ ${symbols.join(', ')} } from '${relativeSpecifier(fileTargetPath, to)}';`; + }) + .join('\n'); + }, + ); + + // Une forme d'import non couverte (namespace, défaut) passerait sans bruit : + // on la remonte explicitement. + for (const match of rewritten.matchAll(ANY_KIT_IMPORT_RE)) { + unresolved.push(`forme d'import non gérée : ${match[0].trim()}`); + } + + return { content: rewritten, unresolved }; +} + +/** À appeler entre deux copies dans un même process de test. */ +export function clearExportMapCache(): void { + exportMapCache.clear(); +} From 9a0c03a1fbdeeb4f998008e1ea5163605edc065b Mon Sep 17 00:00:00 2001 From: LBU Date: Mon, 17 Aug 2026 16:43:42 +0200 Subject: [PATCH 3/7] FSHSP-122 feat(schematics): install in one command, without installing the kit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two commands were needed (`ng add @4sh/ui-kit` for the foundation, then `ng generate @4sh/ui-kit:add` for the components), and the kit stayed in node_modules, where the IDE offered its imports instead of the local copies. Both problems had one cause: the entry point. At `ng add` time the companion was not yet installed, hence a deferred RunSchematicTask, in which an interactive prompt does not hold. Entering through the companion, `ng add @4sh/ui-kit-schematics` chains the foundation and the component selection, and the kit is never installed at all — nothing left to auto-import. The copied version is read from the companion's own `assets/ui-kit-package.json` rather than the consumer's package.json, so `installedKitVersion` goes away. `--skip-components` lays the foundation alone. Library mode is untouched: `npm i @4sh/ui-kit` and import from the package. The kit's facade shrinks to a single ng-add that states which of the two paths was taken — the alternative, silently installing a package with no source copied, is the worst outcome for someone reaching for the obvious command. Its ng-add now saves to dependencies: devDependencies only made sense while it drove the CLI. --- CHANGELOG.md | 15 +++ projects/ui-kit-schematics/package.json | 3 + projects/ui-kit-schematics/src/add/index.ts | 4 +- .../ui-kit-schematics/src/ng-add/index.ts | 48 ++++++---- .../ui-kit-schematics/src/ng-add/schema.d.ts | 6 ++ .../ui-kit-schematics/src/ng-add/schema.json | 15 +++ .../ui-kit-schematics/src/update/index.ts | 6 +- .../src/utils/kit-manifest.ts | 33 +++++++ .../src/utils/package-json.ts | 10 -- projects/ui-kit/collection.json | 15 +-- projects/ui-kit/package.json | 3 - projects/ui-kit/schematics/index.cjs | 93 +++++++------------ scripts/schematics-assets.build.mjs | 20 +--- 13 files changed, 149 insertions(+), 122 deletions(-) create mode 100644 projects/ui-kit-schematics/src/utils/kit-manifest.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 65d3700..18ccb43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,21 @@ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le pr Effets de bord corrigés au passage : les dépendances copiées n'étaient utilisées par personne (copies mortes, suivies par `update` pour rien), le bundle embarquait le composant deux fois, et du code applicatif dépendait d'une `devDependency` — ce qui cassait en `npm ci --omit=dev`. ### Changed +- ⚠️ **Le parcours starter s'installe en une commande, et n'installe plus `@4sh/ui-kit`** (FSHSP-122) : + + ```bash + ng add @4sh/ui-kit-schematics # fondation + sélection des composants, enchaînées + ``` + + Auparavant il en fallait deux (`ng add @4sh/ui-kit` pour la fondation, puis `ng generate @4sh/ui-kit:add` pour les composants), et le kit restait dans `node_modules` : l'auto-complétion de l'IDE proposait ses imports alors que le projet doit utiliser ses copies locales. Les deux problèmes avaient la même cause — la porte d'entrée. Au moment du `ng add`, le compagnon n'était pas encore installé, d'où une `RunSchematicTask` différée, dans laquelle un prompt interactif ne tient pas. + + En entrant par le compagnon, le kit n'est plus installé du tout : plus rien à auto-importer, et le prompt fonctionne dans la foulée de la fondation. La version copiée est lue dans le compagnon lui-même, plus dans le `package.json` du consommateur. + + `--skip-components` pose la fondation seule, pour choisir les composants plus tard. + + **Le mode librairie est inchangé** : `npm i @4sh/ui-kit` puis `import { UiButton } from '@4sh/ui-kit/actions/ui-button'`. Seule la voie « sources copiées » change. `ng add @4sh/ui-kit` installe désormais le kit en `dependencies` (et non plus en `devDependencies`, qui n'avait de sens que pour piloter la CLI) et affiche laquelle des deux voies a été prise. + + **Migration** depuis `0.2.0` : désinstaller `@4sh/ui-kit` du projet (`npm rm @4sh/ui-kit`), les sources copiées n'en dépendant plus. - ⚠️ **L'arborescence copiée est aplatie, et les fichiers transverses sortent de `components/`** (FSHSP-121). La copie reproduisait la structure d'`ng-packagr` (`ui-checkbox/src/lib/ui-checkbox.ts`, plus un barrel `src/public-api.ts`) : `src/` et `lib/` délimitent la surface publiée d'une librairie, et une fois le fichier chez le consommateur ils n'encadrent plus rien — ils enfouissaient chaque composant de deux niveaux. Elles disparaissent, le barrel avec : ``` diff --git a/projects/ui-kit-schematics/package.json b/projects/ui-kit-schematics/package.json index 0916079..dd01edd 100644 --- a/projects/ui-kit-schematics/package.json +++ b/projects/ui-kit-schematics/package.json @@ -19,6 +19,9 @@ "registry": "https://registry.npmjs.org/" }, "schematics": "./collection.json", + "ng-add": { + "save": "devDependencies" + }, "peerDependencies": { "@angular-devkit/core": "^22.0.0", "@angular-devkit/schematics": "^22.0.0", diff --git a/projects/ui-kit-schematics/src/add/index.ts b/projects/ui-kit-schematics/src/add/index.ts index 1dcc35b..b78ddb3 100644 --- a/projects/ui-kit-schematics/src/add/index.ts +++ b/projects/ui-kit-schematics/src/add/index.ts @@ -12,7 +12,7 @@ import { listComponents } from '../utils/component-registry'; import { resolveDependencies } from '../utils/dependency-graph'; import { copyUnit } from '../utils/copy'; import { emptyManifest, readManifest, today, writeManifest } from '../utils/manifest'; -import { installedKitVersion } from '../utils/package-json'; +import { kitVersion as readKitVersion } from '../utils/kit-manifest'; /** * Sélection interactive : checkbox `@inquirer/prompts`, qui supporte déjà @@ -49,7 +49,7 @@ export function add(options: Schema): Rule { return tree; } - const kitVersion = installedKitVersion(tree); + const kitVersion = readKitVersion(); const units = resolveDependencies(selected); const manifest = readManifest(tree) ?? emptyManifest(kitVersion); diff --git a/projects/ui-kit-schematics/src/ng-add/index.ts b/projects/ui-kit-schematics/src/ng-add/index.ts index 55d0e03..9ff3476 100644 --- a/projects/ui-kit-schematics/src/ng-add/index.ts +++ b/projects/ui-kit-schematics/src/ng-add/index.ts @@ -1,8 +1,14 @@ /** - * ng-add — `ng add @4sh/ui-kit` (ou directement `@4sh/ui-kit-schematics`). - * Étape « fondation », une seule fois : dépendances runtime, styles verrouillés - * + éditables, chaîne de génération des tokens, manifeste vide. - * La copie des composants eux-mêmes est le rôle du schematic `add` (FSHSP-109). + * ng-add — `ng add @4sh/ui-kit-schematics`, la commande UNIQUE d'installation + * (FSHSP-122) : fondation (dépendances runtime, styles verrouillés + éditables, + * chaîne de génération des tokens, manifeste) puis sélection et copie des + * composants, enchaînées. + * + * Le point d'entrée est ce package, et non `@4sh/ui-kit`, pour deux raisons + * liées : le kit n'entre jamais dans `node_modules`, donc aucun import ne peut + * viser son code compilé au lieu des copies locales ; et sans package compagnon + * à installer au préalable, plus besoin de la `RunSchematicTask` différée dans + * laquelle un prompt interactif ne tenait pas — d'où la commande unique. */ import { chain, Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; import { updateWorkspace } from '@schematics/angular/utility/workspace'; @@ -12,15 +18,8 @@ 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'; - -/** `projects/ui-kit/package.json` — copié tel quel dans `assets/` : source de vérité - * pour la version du kit et pour les peer dependencies runtime à répercuter. */ -const KIT_PACKAGE_JSON = join(stylesFoundationDir(), '..', 'ui-kit-package.json'); - -function readKitManifestInfo(): { version: string; peerDependencies: Record } { - const json = JSON.parse(readFileSync(KIT_PACKAGE_JSON, 'utf8')); - return { version: json.version, peerDependencies: json.peerDependencies ?? {} }; -} +import { readKitManifestInfo } from '../utils/kit-manifest'; +import { add } from '../add'; /** Copie la fondation de styles selon l'arborescence validée avec le designer * (FSHSP-109) : `ui-kit/` verrouillé (régénéré à chaque `ng add`), `base/` @@ -189,13 +188,30 @@ function createManifest(): Rule { }; } -export function ngAdd(_options: Schema): Rule { - return chain([ +export function ngAdd(options: Schema): Rule { + const foundation = [ copyStylesFoundationRule(), createStyleScaffolds(), copyTokensPipeline(), addRuntimeDependencies(), updateAngularJson(), createManifest(), - ]); + ]; + + // La fondation seule ne rend aucun composant disponible : enchaîner la copie + // est ce qui fait de `ng add` une commande complète. `--skip-components` reste + // là pour poser la fondation dans un projet qui choisira ses composants plus + // tard, ou pour un enchaînement scripté. + if (options.skipComponents) { + return chain([ + ...foundation, + (_tree: Tree, context: SchematicContext) => { + context.logger.info( + 'Fondation posée. Composants à copier ensuite : `ng generate @4sh/ui-kit-schematics:add`.', + ); + }, + ]); + } + + return chain([...foundation, add({ components: options.components, all: options.all })]); } 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 90c7cf7..4607aae 100644 --- a/projects/ui-kit-schematics/src/ng-add/schema.d.ts +++ b/projects/ui-kit-schematics/src/ng-add/schema.d.ts @@ -1,3 +1,9 @@ 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. */ + skipComponents?: boolean; } diff --git a/projects/ui-kit-schematics/src/ng-add/schema.json b/projects/ui-kit-schematics/src/ng-add/schema.json index c872064..2c845b1 100644 --- a/projects/ui-kit-schematics/src/ng-add/schema.json +++ b/projects/ui-kit-schematics/src/ng-add/schema.json @@ -8,6 +8,21 @@ "type": "boolean", "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, + "description": "Copie tous les composants disponibles, sans prompt." + }, + "skipComponents": { + "type": "boolean", + "default": false, + "description": "Pose la fondation seule. Les composants se copient ensuite avec `ng generate @4sh/ui-kit-schematics:add`." } } } diff --git a/projects/ui-kit-schematics/src/update/index.ts b/projects/ui-kit-schematics/src/update/index.ts index a065156..c92b00b 100644 --- a/projects/ui-kit-schematics/src/update/index.ts +++ b/projects/ui-kit-schematics/src/update/index.ts @@ -14,7 +14,7 @@ import type { Schema } from './schema'; import { findUnit } from '../utils/component-registry'; import { renderUnitFiles } from '../utils/copy'; import { readManifest, today, writeManifest } from '../utils/manifest'; -import { installedKitVersion } from '../utils/package-json'; +import { kitVersion as readKitVersion } from '../utils/kit-manifest'; type Action = 'apply' | 'skip' | 'view-diff'; @@ -39,11 +39,11 @@ export function update(options: Schema): Rule { const manifest = readManifest(tree); if (!manifest) { throw new SchematicsException( - "Aucun ui-kit.json trouvé — lancez d'abord `ng add @4sh/ui-kit` puis `ng generate @4sh/ui-kit-schematics:add`.", + "Aucun ui-kit.json trouvé — lancez d'abord `ng add @4sh/ui-kit-schematics`.", ); } - const kitVersion = installedKitVersion(tree); + const kitVersion = readKitVersion(); const outdated = Object.entries(manifest.components).filter(([, entry]) => entry.version !== kitVersion); if (!outdated.length) { diff --git a/projects/ui-kit-schematics/src/utils/kit-manifest.ts b/projects/ui-kit-schematics/src/utils/kit-manifest.ts new file mode 100644 index 0000000..8d1886d --- /dev/null +++ b/projects/ui-kit-schematics/src/utils/kit-manifest.ts @@ -0,0 +1,33 @@ +/** + * kit-manifest — lit `assets/ui-kit-package.json`, la copie du + * `projects/ui-kit/package.json` embarquée par `scripts/schematics-assets.build.mjs`. + * + * C'est la source de vérité de la version du kit **et** des peer dependencies + * runtime à répercuter chez le consommateur. + * + * Elle vit dans ce package, pas dans le `package.json` du consommateur : le + * parcours starter n'installe plus `@4sh/ui-kit` (FSHSP-122), justement pour + * qu'aucun import ne puisse le viser depuis `node_modules`. Il n'y a donc plus + * de version à y lire, et les deux packages étant publiés en lockstep, celle-ci + * est exactement celle du kit dont les sources sont copiées. + */ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { stylesFoundationDir } from './component-registry'; + +const KIT_PACKAGE_JSON = join(stylesFoundationDir(), '..', 'ui-kit-package.json'); + +export interface KitManifestInfo { + version: string; + peerDependencies: Record; +} + +export function readKitManifestInfo(): KitManifestInfo { + const json = JSON.parse(readFileSync(KIT_PACKAGE_JSON, 'utf8')); + return { version: json.version, peerDependencies: json.peerDependencies ?? {} }; +} + +/** Version du kit dont ce package embarque les sources. */ +export function kitVersion(): string { + return readKitManifestInfo().version; +} diff --git a/projects/ui-kit-schematics/src/utils/package-json.ts b/projects/ui-kit-schematics/src/utils/package-json.ts index 8bc2590..035dcd4 100644 --- a/projects/ui-kit-schematics/src/utils/package-json.ts +++ b/projects/ui-kit-schematics/src/utils/package-json.ts @@ -19,16 +19,6 @@ export function writePackageJson(tree: Tree, json: PackageJson): void { tree.overwrite('/package.json', JSON.stringify(json, null, 2) + '\n'); } -/** Version installée de `@4sh/ui-kit` (dependencies ou devDependencies), sans le préfixe semver (`^`, `~`). */ -export function installedKitVersion(tree: Tree): string { - const json = readPackageJson(tree); - const raw = json.dependencies?.['@4sh/ui-kit'] ?? json.devDependencies?.['@4sh/ui-kit']; - if (!raw) { - throw new SchematicsException("@4sh/ui-kit n'est pas installé — lancez d'abord `ng add @4sh/ui-kit`."); - } - return String(raw).replace(/^[\^~]/, ''); -} - export function addDependency( tree: Tree, name: string, diff --git a/projects/ui-kit/collection.json b/projects/ui-kit/collection.json index 1f8ab76..a9956a3 100644 --- a/projects/ui-kit/collection.json +++ b/projects/ui-kit/collection.json @@ -2,19 +2,8 @@ "$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json", "schematics": { "ng-add": { - "description": "Prépare le projet : installe @4sh/ui-kit-schematics puis lance sa fondation (styles, tokens, angular.json).", - "factory": "./schematics/index.cjs#ngAdd", - "schema": "./schematics/schema-ng-add.json" - }, - "add": { - "description": "Copie un ou plusieurs composants ui-* (et leurs dépendances) dans le projet.", - "factory": "./schematics/index.cjs#add", - "schema": "./schematics/schema-add.json" - }, - "update": { - "description": "Compare les composants déjà copiés à la version installée du kit et propose de les mettre à jour.", - "factory": "./schematics/index.cjs#update", - "schema": "./schematics/schema-update.json" + "description": "Installe le kit en mode librairie. Pour copier les sources : ng add @4sh/ui-kit-schematics.", + "factory": "./schematics/index.cjs#ngAdd" } } } diff --git a/projects/ui-kit/package.json b/projects/ui-kit/package.json index ca1d09c..8f2922b 100644 --- a/projects/ui-kit/package.json +++ b/projects/ui-kit/package.json @@ -28,9 +28,6 @@ "registry": "https://registry.npmjs.org/" }, "schematics": "./collection.json", - "ng-add": { - "save": "devDependencies" - }, "peerDependencies": { "@angular/cdk": "^22.0.0", "@angular/common": "^22.0.0", diff --git a/projects/ui-kit/schematics/index.cjs b/projects/ui-kit/schematics/index.cjs index 0f4dcca..db70f7c 100644 --- a/projects/ui-kit/schematics/index.cjs +++ b/projects/ui-kit/schematics/index.cjs @@ -1,11 +1,21 @@ /** - * Façade de schematics de `@4sh/ui-kit` (FSHSP-109). + * Façade de schematics de `@4sh/ui-kit` — réduite à une redirection (FSHSP-122). * - * Le kit publié ne contient que du code COMPILÉ : les sources brutes que le - * starter recopie chez le consommateur vivent dans le package compagnon - * `@4sh/ui-kit-schematics`. Ce fichier n'existe que pour offrir un nom unique - * au consommateur — `ng add @4sh/ui-kit`, `ng generate @4sh/ui-kit:add` — et - * délègue tout le travail au compagnon. Aucune logique métier ici. + * `@4sh/ui-kit` a deux usages, et un seul passe encore par ici : + * + * - **mode librairie** : `npm i @4sh/ui-kit`, puis + * `import { UiButton } from '@4sh/ui-kit/actions/ui-button'`. C'est ce que + * fait `ng add @4sh/ui-kit` : installer le package. Rien d'autre à générer. + * - **mode starter** (sources copiées, façon shadcn/spartan-ng) : le point + * d'entrée est `ng add @4sh/ui-kit-schematics`. Le kit n'y est délibérément + * PAS installé — sans lui dans `node_modules`, aucun import ne peut viser son + * code compilé à la place des copies locales, et l'auto-complétion de l'IDE + * ne propose plus que ces dernières. + * + * Ce fichier n'existe donc plus que pour dire laquelle des deux voies on a + * prise : `ng add @4sh/ui-kit` est la commande que l'on tente naturellement pour + * le starter, et se retrouver avec un package installé sans une seule source + * copiée, sans un mot d'explication, serait la pire des issues. * * ⚠️ CommonJS et extension `.cjs` OBLIGATOIRES : ng-packagr ajoute * `"type": "module"` au package.json publié, donc un `.js` serait interprété @@ -14,63 +24,28 @@ * sans extension n'essaie que `.js`/`.json`/`.node`, jamais `.cjs`. */ -const { externalSchematic, SchematicsException } = require('@angular-devkit/schematics'); -const { NodePackageInstallTask, RunSchematicTask } = require('@angular-devkit/schematics/tasks'); - -/** Le compagnon est versionné en lockstep avec le kit : même version publiée. */ const COMPANION = '@4sh/ui-kit-schematics'; -const COMPANION_VERSION = `^${require('../package.json').version}`; -/** - * `ng add @4sh/ui-kit`. - * - * Le compagnon n'est pas encore installé à ce stade, donc pas d'appel direct : - * on l'inscrit en devDependency, on programme l'installation, puis on demande - * son `ng-add` en TÂCHE dépendante de cette installation. `RunSchematicTask` - * résout la collection au moment de son exécution — après l'install — là où - * `externalSchematic` la résoudrait tout de suite, et échouerait. - * (Même enchaînement que le `ng-add` d'`@angular/material`.) - */ -function ngAdd(options) { +/** `ng add @4sh/ui-kit` — le package est installé par la CLI avant d'arriver ici. */ +function ngAdd() { return (tree, context) => { - const pkg = JSON.parse(tree.read('/package.json').toString('utf8')); - pkg.devDependencies = { ...pkg.devDependencies, [COMPANION]: COMPANION_VERSION }; - tree.overwrite('/package.json', JSON.stringify(pkg, null, 2) + '\n'); - - const installId = context.addTask(new NodePackageInstallTask()); - context.addTask(new RunSchematicTask(COMPANION, 'ng-add', options ?? {}), [installId]); - + context.logger.info( + [ + '', + '@4sh/ui-kit est installé : les composants sont utilisables tels quels,', + " import { UiButton } from '@4sh/ui-kit/actions/ui-button';", + '', + `Pour COPIER les sources dans le projet et les posséder, c'est l'autre voie :`, + ` ng add ${COMPANION}`, + '', + `Elle n'installe pas @4sh/ui-kit, volontairement : hors de node_modules, aucun`, + 'import ne peut viser son code compilé au lieu des copies locales. Les deux', + 'modes ne se combinent pas — choisir celui qui correspond au projet.', + '', + ].join('\n'), + ); return tree; }; } -/** - * Délégation directe, pour les schematics lancés APRÈS `ng add` : le compagnon - * est alors présent dans `node_modules`, `externalSchematic` le résout donc - * immédiatement — et le prompt interactif reste au premier plan, ce qu'une - * tâche différée ne permettrait pas. - */ -function delegate(schematicName) { - return (options) => () => { - // Vérification AVANT de rendre la règle : `externalSchematic` ne résout la - // collection qu'à l'exécution de la règle qu'il renvoie, donc l'entourer - // d'un try/catch ici n'attraperait rien (le message resterait le laconique - // « Collection ... cannot be resolved »). On teste donc la résolution - // nous-mêmes, pour pouvoir dire quoi faire. - try { - require.resolve(`${COMPANION}/package.json`); - } catch { - throw new SchematicsException( - `${COMPANION} est introuvable. Ce package porte les sources copiées par le starter ; ` + - `il est installé par \`ng add @4sh/ui-kit\` — lancez cette commande d'abord.`, - ); - } - return externalSchematic(COMPANION, schematicName, options ?? {}); - }; -} - -module.exports = { - ngAdd, - add: delegate('add'), - update: delegate('update'), -}; +module.exports = { ngAdd }; diff --git a/scripts/schematics-assets.build.mjs b/scripts/schematics-assets.build.mjs index 790ceb2..99c9fc1 100644 --- a/scripts/schematics-assets.build.mjs +++ b/scripts/schematics-assets.build.mjs @@ -134,22 +134,10 @@ function main() { // peerDependencies runtime à répercuter chez le consommateur (ng-add). copyFileSync(join(KIT, 'package.json'), join(ASSETS, 'ui-kit-package.json')); - // Schémas de la FAÇADE (`projects/ui-kit/collection.json`, qui donne au - // consommateur un nom unique — voir projects/ui-kit/schematics/index.cjs). - // La façade ne fait que déléguer, mais doit déclarer les mêmes options que - // le compagnon : c'est elle que la CLI lit pour typer les arguments - // (`--components` doit arriver en tableau, pas en chaîne). On les COPIE - // depuis le compagnon plutôt que de les réécrire : une option ajoutée d'un - // seul côté serait silencieusement ignorée par l'autre. - const facadeSchemas = [ - ['src/ng-add/schema.json', 'schema-ng-add.json'], - ['src/add/schema.json', 'schema-add.json'], - ['src/update/schema.json', 'schema-update.json'], - ]; - const facadeDir = join(ROOT, 'projects/ui-kit/schematics'); - for (const [from, to] of facadeSchemas) { - copyFileSync(join(ROOT, 'projects/ui-kit-schematics', from), join(facadeDir, to)); - } + // Plus aucun schéma de façade à synchroniser (FSHSP-122) : la façade de + // `@4sh/ui-kit` ne délègue plus rien — elle se borne à indiquer que le + // parcours starter passe par `ng add @4sh/ui-kit-schematics` — et n'expose + // donc aucune option. Les schémas vivent d'un seul côté, celui du compagnon. // Chaîne de génération des tokens — embarquée entière (décision validée avec // le designer, FSHSP-109) : `generated/` doit rester régénérable chez le From 5961a17e02552e33bfa9570b1eb6c935adc9f05c Mon Sep 17 00:00:00 2001 From: LBU Date: Mon, 17 Aug 2026 16:47:24 +0200 Subject: [PATCH 4/7] FSHSP-122 docs(schematics): document the one-command starter workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The companion's READMEs told the reader never to install it directly — it is now the entry point, so both were wrong on their central claim. They document the two consumption modes instead, and why the kit is absent from the starter one. PUBLISHING and VERSIONING carried a lockstep rationale that no longer holds: the kit's facade used to require the companion as `^`. Nothing references anything at install time now. The shared number still matters, for a different reason — it identifies which kit a copied file came from, via the traceability header and ui-kit.json — and the publish order's justification is rewritten to match: a partial release no longer breaks consumers, it only misleads them. The local-testing recipe drops the kit tarball, which the starter path no longer installs, and gains the two checks worth running afterwards. --- .github/workflows/publish-ui-kit.yml | 36 ++++++----- docs/PUBLISHING.md | 73 ++++++++++++++++------- docs/VERSIONING.md | 15 +++-- projects/ui-kit-schematics/README.fr.md | 79 ++++++++++++++++--------- projects/ui-kit-schematics/README.md | 76 +++++++++++++++--------- 5 files changed, 180 insertions(+), 99 deletions(-) diff --git a/.github/workflows/publish-ui-kit.yml b/.github/workflows/publish-ui-kit.yml index 91d465b..8e1f7dc 100644 --- a/.github/workflows/publish-ui-kit.yml +++ b/.github/workflows/publish-ui-kit.yml @@ -1,10 +1,11 @@ name: Publish @4sh/ui-kit to npm # Publishes TWO packages in lockstep, from the same approved job: -# @4sh/ui-kit — the compiled components +# @4sh/ui-kit — the compiled components (library mode) # @4sh/ui-kit-schematics — the raw sources the starter copies (FSHSP-109) -# They always carry the same version (stamped from the kit at assembly time), -# because the kit's ng-add facade requests `^` of the companion. +# They always carry the same version (stamped from the kit at assembly time): +# the companion embeds a copy of the kit's sources, and that shared number is what +# identifies which kit a copied file came from (FSHSP-122). # Manual trigger only: publishing is irreversible (a name+version can never be # reused, and `npm unpublish` is limited to 72h), so it never fires on a push. @@ -73,10 +74,10 @@ jobs: - name: Build the schematics package run: npm run schematics:build - # Les deux packages partent ensemble et portent le MÊME numéro : la façade - # de `@4sh/ui-kit` demande le compagnon en `^` (voir - # projects/ui-kit/schematics/index.cjs). Un décalage ici casserait - # `ng add` chez le consommateur, sans que rien ne le signale côté kit. + # Les deux packages partent ensemble et portent le MÊME numéro : c'est lui + # qui identifie de quel kit vient un fichier copié (en-tête de traçabilité + # + ui-kit.json, dont `update` se sert pour ses diffs). Un décalage ici + # rendrait cette provenance fausse, sans que rien ne le signale. - name: Check both packages carry the same version run: | kit=$(node -p "require('./dist/ui-kit/package.json').version") @@ -197,16 +198,19 @@ jobs: # no notion of a transaction: the second publish can fail on its own. The # order decides what a half-published release leaves behind. # - # companion then kit (this order) — if the companion fails, the kit is - # never published and nothing on the registry references a missing - # package. If the kit then fails, the orphan companion version is - # inert: no released kit points at it. - # kit then companion — a companion failure would strand a published kit - # whose facade requests `^` of something that does not exist, - # breaking `ng add` for everyone, with no way to unpublish after 72h. + # Since FSHSP-122 neither package references the other at install time, so + # a partial release no longer breaks anyone — it used to strand a published + # kit whose facade required a companion that did not exist, making `ng add` + # fail for everyone. What is left at stake is narrower: # - # Both failure modes are recoverable, but only one of them is invisible to - # consumers. Do not swap these two steps. + # companion then kit (this order) — an orphan companion ships sources + # whose header announces a kit version absent from the registry. + # kit then companion — an orphan kit leaves the starter path copying the + # PREVIOUS version's sources, silently, while library mode moved on. + # + # The first is the more legible failure: the version it names simply is not + # there yet, rather than a starter quietly one release behind. Either way + # the fix is to publish the missing half, or a patch. Do not swap these. - name: Publish @4sh/ui-kit-schematics run: npm publish --provenance working-directory: dist/ui-kit-schematics diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md index 2728a18..bfd2f8b 100644 --- a/docs/PUBLISHING.md +++ b/docs/PUBLISHING.md @@ -13,24 +13,42 @@ Versionnage : voir [`VERSIONING.md`](./VERSIONING.md). | `@4sh/ui-kit-schematics` | les **sources brutes** que le starter recopie chez le consommateur, et les schematics qui les copient | `projects/ui-kit-schematics` | Le second existe parce que `ng-packagr` *inline* template et SCSS dans le `.mjs` -publié : les sources que `ng generate @4sh/ui-kit:add` doit copier n'existent -nulle part dans le tarball du kit (FSHSP-109). +publié : les sources que les schematics doivent copier n'existent nulle part dans +le tarball du kit (FSHSP-109). + +Chacun sert **un mode de consommation**, et les deux ne se croisent pas +(FSHSP-122) : + +| Mode | Commande | Ce que le consommateur installe | +|---|---|---| +| Librairie | `npm i @4sh/ui-kit` | le kit compilé | +| Starter (sources copiées) | `ng add @4sh/ui-kit-schematics` | le compagnon seul — **jamais le kit** | + +Le kit est absent du parcours starter délibérément : hors de `node_modules`, aucun +import ne peut viser son code compilé au lieu des copies locales. **Les deux portent toujours le même numéro de version, et se publient dans le -même job.** Le `ng-add` de `@4sh/ui-kit` réclame le compagnon en -`^` ([`projects/ui-kit/schematics/index.cjs`](../projects/ui-kit/schematics/index.cjs)) : -publier l'un sans l'autre casse `ng add` chez le consommateur. La version du -compagnon est d'ailleurs **estampillée depuis celle du kit** au moment de -l'assemblage (`scripts/schematics-package.build.mjs`) — il n'y a pas de numéro à -tenir à jour à deux endroits, et le job `verify` vérifie la parité avant toute -publication. - -> ⚠️ **Ordre de publication : le compagnon d'abord, le kit ensuite.** npm ne -> connaît pas la transaction ; si la seconde publication échoue, l'ordre décide -> de ce qui reste. Un compagnon orphelin est inerte (aucun kit publié ne le -> référence) ; un kit publié sans son compagnon est cassé pour tout le monde, et -> irréparable passé 72 h. Le workflow applique cet ordre, avec le raisonnement -> en commentaire — ne pas l'inverser. +même job.** La version du compagnon est **estampillée depuis celle du kit** au +moment de l'assemblage (`scripts/schematics-package.build.mjs`) — il n'y a pas de +numéro à tenir à jour à deux endroits, et le job `verify` vérifie la parité avant +toute publication. + +Ce numéro commun n'est pas cosmétique : le compagnon embarque une copie des +sources du kit, et c'est lui qui identifie **de quel kit** vient un fichier copié. +Il est inscrit dans l'en-tête de traçabilité de chaque fichier et dans le +`ui-kit.json` du consommateur, dont `update` se sert pour calculer ses diffs. + +> ⚠️ **Ordre de publication : le compagnon d'abord, le kit ensuite.** Le workflow +> applique cet ordre — ne pas l'inverser sans relire ce qui suit. +> +> Depuis FSHSP-122, aucun des deux packages ne référence l'autre à l'installation : +> une publication partielle ne casse donc plus personne, là où un kit publié sans +> son compagnon rendait auparavant `ng add` inopérant pour tout le monde. Ce qui +> reste en jeu est plus étroit : un compagnon publié seul livre des sources dont +> l'en-tête annonce une version de kit absente du registre, et un kit publié seul +> laisse le parcours starter sur les sources de la version précédente. L'ordre +> actuel privilégie le second cas, moins déroutant — mais dans les deux +> situations, la réponse est de publier le manquant, ou un correctif. --- @@ -314,16 +332,25 @@ Contenu et résolution des imports strictement identiques à une vraie publication. Pour du développement en parallèle : `npm install ../starter-angular/dist/ui-kit`. -Pour éprouver le **starter** (`ng add`, `ng generate …:add`), il faut les deux -packages, le kit ne sachant que déléguer : +Pour éprouver le **starter**, le compagnon suffit — il porte les sources *et* les +schematics, et le parcours n'installe pas le kit : ```bash npm run schematics:pack # → 4sh-ui-kit-schematics-.tgz -npm run ui-kit:pack # → 4sh-ui-kit-.tgz -# dans le projet consommateur — le compagnon d'abord, sinon `ng add` tirerait -# la version publiée sur le registre au lieu de celle qu'on veut éprouver +# dans le projet consommateur npm install -D /chemin/vers/4sh-ui-kit-schematics-.tgz -npm install -D /chemin/vers/4sh-ui-kit-.tgz -ng generate @4sh/ui-kit:add +npx ng generate @4sh/ui-kit-schematics:ng-add --components ui-button ui-checkbox +``` + +On passe par `ng generate …:ng-add` plutôt que `ng add` : la commande publiée +`ng add @4sh/ui-kit-schematics` irait chercher le package sur le registre, pas le +tarball local. La règle exécutée est exactement la même. Et `--components` évite +le prompt interactif, ce qui rend l'essai scriptable — l'omettre le rétablit. + +Vérifications qui valent la peine, une fois la commande passée : + +```bash +grep '@4sh' package.json # le kit doit être ABSENT, seul le compagnon apparaît +npm install && npx ng build # les sources copiées doivent compiler telles quelles ``` diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index 6e8ed05..fe717d6 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -7,8 +7,8 @@ Format: `MAJOR.MINOR.PATCH` ## Scope: one version number, two published packages SemVer applies to **`projects/ui-kit/package.json`** only — that is the artifact -published as `@4sh/ui-kit`, the one a consumer's `package.json` pins a version -against. +published as `@4sh/ui-kit`, the one a library-mode consumer's `package.json` pins a +version against. `@4sh/ui-kit-schematics` (the raw sources the starter copies, see [`PUBLISHING.md`](./PUBLISHING.md)) is published from the same repo, at the same @@ -17,10 +17,13 @@ rather than maintained separately. It has no version of its own to bump: the number written in `projects/ui-kit-schematics/package.json` is a development placeholder and is overwritten by the build. -That lockstep is not cosmetic. The kit's `ng-add` requests the companion as -`^`, so the two moving independently would break `ng add` for -consumers. It also means the table below is read against the **kit**: a change -confined to the schematics still ships under the kit's next version. +That lockstep is not cosmetic. The companion embeds a copy of the kit's sources, +and the shared number is what identifies **which kit** a copied file came from: it +is written into every copied file's traceability header and into the consumer's +`ui-kit.json`, which `update` reads to compute its diffs. Two numbers drifting +apart would make that provenance meaningless. It also means the table below is read +against the **kit**: a change confined to the schematics still ships under the +kit's next version. The root `package.json` (demo app + Storybook tooling) is **not** versioned in step with it. Nothing depends on it: it is never published, `private: true`, and diff --git a/projects/ui-kit-schematics/README.fr.md b/projects/ui-kit-schematics/README.fr.md index 03eb5f5..93c79ea 100644 --- a/projects/ui-kit-schematics/README.fr.md +++ b/projects/ui-kit-schematics/README.fr.md @@ -11,47 +11,72 @@ Angular qui les recopient dans un projet consommateur. --- -## ⚠️ Ne pas installer ce package directement - -Vous ne l'installez jamais vous-même, et vous n'épinglez jamais sa version. Il -est tiré automatiquement par le kit : +## Installation ```bash -ng add @4sh/ui-kit +ng add @4sh/ui-kit-schematics ``` -Tout est documenté sur -[`@4sh/ui-kit`](https://www.npmjs.com/package/@4sh/ui-kit) — c'est ce package -qu'il faut lire, et le seul dont dépendre. +Une seule commande : elle pose la fondation (styles, design tokens, +`angular.json`), puis demande quels composants copier et les copie, dépendances +comprises. ---- +``` +src/app/shared/ +├── components/ui/{catégorie}/{ui-nom}/{ui-nom}.ts ← uniquement des composants +└── ui-core/{forms|motion|overlay|theming|types}/ ← directives de base, services, utilitaires, types +``` -## Pourquoi un package séparé +Les fichiers copiés vous appartiennent : modifiez-les librement. `ui-kit.json` +retient de quelle version vient chaque composant, ce qui permet ensuite à `update` +de vous présenter un diff fichier par fichier face à des sources plus récentes — +jamais une fusion automatique. -`ng-packagr` *inline* template et SCSS dans le `.mjs` publié. Les sources que -`ng generate @4sh/ui-kit:add` doit copier n'existent donc nulle part dans le -tarball du kit. Les garder ici a une conséquence voulue : un consommateur -classique de `@4sh/ui-kit`, qui importe seulement les composants compilés, ne les -télécharge jamais. +| | | +|---|---| +| `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 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 | -Le kit expose une façade de schematics sans aucune logique, qui délègue à ce -package. C'est toute la relation entre les deux. +### `@4sh/ui-kit` n'est délibérément **pas** installé -## Contenu +Cette voie ne met jamais le kit dans `node_modules`, et c'est précisément le but : +absent, aucun import ne peut viser son code compilé au lieu de vos copies +locales — ni dans les sources copiées, ni dans l'auto-complétion de votre éditeur. -| | | -|---|---| -| `ng-add` | fondation : styles, tokens, `angular.json` | -| `add` | copie les composants dans le projet (sélection interactive, ou `--all`) | -| `update` | diff par composant face aux sources publiées | -| `assets/` | les sources brutes des composants, et la chaîne de génération des tokens | +## L'autre voie : l'utiliser comme librairie + +Si vous préférez consommer les composants compilés, sans posséder aucune source : + +```bash +npm i @4sh/ui-kit +``` + +```ts +import { UiButton } from '@4sh/ui-kit/actions/ui-button'; +``` + +Rien n'est copié, rien n'est à vous : vous suivez les releases du kit. Les deux +modes ne se combinent pas — choisissez celui qui correspond au projet. + +--- + +## Pourquoi un package séparé + +`ng-packagr` *inline* template et SCSS dans le `.mjs` publié. Les sources que ces +schematics copient n'existent donc nulle part dans le tarball du kit, d'où leur +présence ici. Cette séparation a une conséquence voulue, dans les deux sens : un +consommateur en mode librairie ne télécharge jamais les sources brutes, et un +consommateur en mode starter ne télécharge jamais le kit compilé. ## Versionnage **Les deux packages portent toujours le même numéro de version**, estampillé -depuis celui du kit à l'assemblage — la façade du kit réclame le compagnon en -`^`. Il n'y a rien à tenir à jour à la main, ni aucune raison -d'épingler ce package vous-même. +depuis celui du kit à l'assemblage. Ce package embarque une copie des sources du +kit, et ce numéro commun est ce qui identifie *de quel* kit vient un fichier +copié — il est inscrit dans l'en-tête de traçabilité de chaque fichier, et dans +`ui-kit.json`. Voir [`docs/VERSIONING.md`](https://github.com/4sh/starter-angular/blob/main/docs/VERSIONING.md) et [`docs/PUBLISHING.md`](https://github.com/4sh/starter-angular/blob/main/docs/PUBLISHING.md). diff --git a/projects/ui-kit-schematics/README.md b/projects/ui-kit-schematics/README.md index 08c2de9..f723a29 100644 --- a/projects/ui-kit-schematics/README.md +++ b/projects/ui-kit-schematics/README.md @@ -11,47 +11,69 @@ schematics that copy them into a consuming project. --- -## ⚠️ Do not install this package directly +## Install -You never install it yourself, and you never pin its version. It is pulled in -automatically by the kit: +```bash +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. + +``` +src/app/shared/ +├── components/ui/{category}/{ui-name}/{ui-name}.ts ← components only +└── ui-core/{forms|motion|overlay|theming|types}/ ← base directives, services, utils, types +``` + +Copied files belong to you: edit them freely. `ui-kit.json` records which +component came from which version, so `update` can later show you a per-file diff +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 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 | + +### `@4sh/ui-kit` is deliberately **not** installed + +This path never puts the kit in `node_modules`, and that is the point: with it +absent, no import can reach its compiled code instead of your local copies — not +in the copied sources, and not in your editor's auto-import either. + +## The other way: use it as a library + +If you would rather consume compiled components and not own any source: ```bash -ng add @4sh/ui-kit +npm i @4sh/ui-kit +``` + +```ts +import { UiButton } from '@4sh/ui-kit/actions/ui-button'; ``` -Everything is documented on -[`@4sh/ui-kit`](https://www.npmjs.com/package/@4sh/ui-kit) — that is the package -to read, and the only one to depend on. +Nothing is copied, nothing is yours to edit — you follow the kit's releases. The +two modes do not combine: pick the one that fits the project. --- ## Why a separate package `ng-packagr` *inlines* templates and SCSS into the published `.mjs`. The sources -that `ng generate @4sh/ui-kit:add` has to copy therefore exist nowhere in the -kit's own tarball. Keeping them here has a deliberate consequence: a regular -consumer of `@4sh/ui-kit`, who only imports compiled components, never downloads -them. - -The kit exposes a schematics facade holding no logic, which delegates to this -package. That is the whole relationship between the two. - -## Contents - -| | | -|---|---| -| `ng-add` | foundation: styles, tokens, `angular.json` | -| `add` | copies components into the project (interactive selection, or `--all`) | -| `update` | per-component diff against the published sources | -| `assets/` | the raw component sources, plus the token generation chain | +these schematics copy therefore exist nowhere in the kit's own tarball, which is +why they live here. That split has a deliberate consequence in both directions: a +library-mode consumer never downloads the raw sources, and a starter-mode consumer +never downloads the compiled kit. ## Versioning -**The two packages always carry the same version number**, stamped from the -kit's at assembly time — the kit's facade requires the companion in -`^`. There is nothing to keep in sync by hand, and no reason to -pin this package yourself. +**The two packages always carry the same version number**, stamped from the kit's +at assembly time. This package embeds a copy of the kit's sources, and that shared +number is what identifies *which* kit a copied file came from — it is written into +the traceability header of every copied file, and into `ui-kit.json`. See [`docs/VERSIONING.md`](https://github.com/4sh/starter-angular/blob/main/docs/VERSIONING.md) and [`docs/PUBLISHING.md`](https://github.com/4sh/starter-angular/blob/main/docs/PUBLISHING.md). From 0120f0fb93a4360fe83259d682024102e6f5a894 Mon Sep 17 00:00:00 2001 From: LBU Date: Mon, 17 Aug 2026 17:15:57 +0200 Subject: [PATCH 5/7] FSHSP-122 fix(schematics): install the runtime dependencies ng-add declares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ng add @4sh/ui-kit-schematics` wrote @angular/cdk and fontawesome into dependencies and pointed angular.json at their CSS, then installed neither: the build failed on two `Could not resolve` errors with nothing naming the cause. `main` scheduled a NodePackageInstallTask in the kit's facade; emptying that facade removed the only one in the repo, and the companion's ng-add never had it. `ng add` installs the package you name, not what its schematic adds afterwards. `--skip-install` now gates that task, instead of being declared and ignored. Two silent-loss paths in copy, in a module that throws everywhere else: `endsWith('public-api.ts')` also matched a legitimate `*-public-api.ts` component file and dropped it; and flattening can map two sources onto one target, where the second overwrote the first without a word — it now throws, naming both. Drops `clearExportMapCache`, exported for tests that do not exist. --- .../ui-kit-schematics/src/ng-add/index.ts | 26 ++++++++++++++++++- projects/ui-kit-schematics/src/utils/copy.ts | 20 +++++++++++++- .../ui-kit-schematics/src/utils/export-map.ts | 6 ++++- .../src/utils/rewrite-imports.ts | 5 ---- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/projects/ui-kit-schematics/src/ng-add/index.ts b/projects/ui-kit-schematics/src/ng-add/index.ts index 9ff3476..336da37 100644 --- a/projects/ui-kit-schematics/src/ng-add/index.ts +++ b/projects/ui-kit-schematics/src/ng-add/index.ts @@ -11,6 +11,7 @@ * laquelle un prompt interactif ne tenait pas — d'où la commande unique. */ import { chain, Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; import { updateWorkspace } from '@schematics/angular/utility/workspace'; import { readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; @@ -188,6 +189,23 @@ function createManifest(): Rule { }; } +/** + * Programme le `npm install` des dépendances que `addRuntimeDependencies` vient + * d'inscrire. + * + * Sans lui, `angular.json` référence `node_modules/@angular/cdk/overlay-prebuilt.css` + * et la feuille FontAwesome alors que ni l'un ni l'autre n'est installé : le + * projet ne compile pas, et l'erreur (`Could not resolve`) ne dit rien de sa + * cause. C'est le seul install du parcours — `ng add` n'installe que le package + * qu'on lui nomme, pas ce que son schematic ajoute ensuite au `package.json`. + */ +function installRuntimeDependencies(): Rule { + return (tree: Tree, context: SchematicContext) => { + context.addTask(new NodePackageInstallTask()); + return tree; + }; +} + export function ngAdd(options: Schema): Rule { const foundation = [ copyStylesFoundationRule(), @@ -198,6 +216,11 @@ export function ngAdd(options: Schema): Rule { createManifest(), ]; + // 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). + const install = options.skipInstall ? [] : [installRuntimeDependencies()]; + // La fondation seule ne rend aucun composant disponible : enchaîner la copie // est ce qui fait de `ng add` une commande complète. `--skip-components` reste // là pour poser la fondation dans un projet qui choisira ses composants plus @@ -210,8 +233,9 @@ export function ngAdd(options: Schema): Rule { 'Fondation posée. Composants à copier ensuite : `ng generate @4sh/ui-kit-schematics:add`.', ); }, + ...install, ]); } - return chain([...foundation, add({ components: options.components, all: options.all })]); + return chain([...foundation, add({ components: options.components, all: options.all }), ...install]); } diff --git a/projects/ui-kit-schematics/src/utils/copy.ts b/projects/ui-kit-schematics/src/utils/copy.ts index 5b28110..9312e0d 100644 --- a/projects/ui-kit-schematics/src/utils/copy.ts +++ b/projects/ui-kit-schematics/src/utils/copy.ts @@ -6,6 +6,7 @@ import type { Tree } from '@angular-devkit/schematics'; 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 { BARREL_FILENAME } from './export-map'; @@ -42,15 +43,32 @@ export function renderUnitFiles(unit: AssetUnit, kitVersion: string): RenderedFi const files: RenderedFile[] = []; const unresolved: string[] = []; + // L'aplatissement retire `src/` et `lib/` : deux sources distinctes peuvent + // donc viser la même destination (`src/foo.ts` et `src/lib/foo.ts`). Sans ce + // relevé, la seconde écraserait la première en silence — le seul chemin de + // perte muette d'un module qui échoue bruyamment partout ailleurs. + const claimedBy = new Map(); + for (const absSrc of unitSourceFiles(unit)) { // Le barrel est une surface de publication de librairie : il n'a rien à // faire chez le consommateur, où les imports désignent les fichiers. - if (absSrc.endsWith(BARREL_FILENAME)) continue; + // 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; const relPath = flattenedRelPath(unit, absSrc); const ext = absSrc.slice(absSrc.lastIndexOf('.')); const targetPath = `${unit.targetDir}/${relPath}`; + const previous = claimedBy.get(targetPath); + if (previous) { + throw new SchematicsException( + `${unit.name} : après aplatissement, « ${previous} » et « ${absSrc} » visent tous deux ` + + `${targetPath}. Renommer l'un des deux dans le kit — la copie ne peut pas trancher.`, + ); + } + claimedBy.set(targetPath, absSrc); + let source = readFileSync(absSrc, 'utf8'); if (ext === '.ts') { const result = rewriteKitImports(source, targetPath); diff --git a/projects/ui-kit-schematics/src/utils/export-map.ts b/projects/ui-kit-schematics/src/utils/export-map.ts index 99fef06..32d0c18 100644 --- a/projects/ui-kit-schematics/src/utils/export-map.ts +++ b/projects/ui-kit-schematics/src/utils/export-map.ts @@ -15,6 +15,7 @@ * plutôt que de produire un import silencieusement faux. */ import { readFileSync } from 'node:fs'; +import { basename } from 'node:path'; import type { AssetUnit } from './component-registry'; import { unitSourceFiles, flattenedRelPath } from './component-registry'; @@ -71,7 +72,10 @@ export function buildExportMap(unit: AssetUnit): Map { const map = new Map(); for (const absPath of unitSourceFiles(unit)) { - if (absPath.endsWith(BARREL_FILENAME)) { + // Nom EXACT, comme dans `copy.ts` : sur un `endsWith`, un + // `ui-table-public-api.ts` serait pris pour un barrel et ferait échouer + // `assertBarrelIsTrivial` sur sa première déclaration. + if (basename(absPath) === BARREL_FILENAME) { assertBarrelIsTrivial(unit, absPath); continue; } diff --git a/projects/ui-kit-schematics/src/utils/rewrite-imports.ts b/projects/ui-kit-schematics/src/utils/rewrite-imports.ts index 4aa710a..8bc4352 100644 --- a/projects/ui-kit-schematics/src/utils/rewrite-imports.ts +++ b/projects/ui-kit-schematics/src/utils/rewrite-imports.ts @@ -116,8 +116,3 @@ export function rewriteKitImports(content: string, fileTargetPath: string): Rewr return { content: rewritten, unresolved }; } - -/** À appeler entre deux copies dans un même process de test. */ -export function clearExportMapCache(): void { - exportMapCache.clear(); -} From dbee1d758830539d68e3f32ab05ca7858baa2b8a Mon Sep 17 00:00:00 2001 From: LBU Date: Mon, 17 Aug 2026 17:15:57 +0200 Subject: [PATCH 6/7] FSHSP-122 docs(schematics): let each README own its mode and point at the other MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The companion's README documented library mode inline — install command and import sample — which `@4sh/ui-kit` already covers on its own npm page. It now points there in two lines instead of restating it. The reverse pointer was missing entirely, and mattered more: the kit's README only ever described library mode, so now that the starter path enters through the companion, a reader of the kit's npm page had no way to discover that copying the sources is an option at all. It gets a short section naming the command and why the kit is absent from that path. Also lists `--skip-install`, now that it does something. --- projects/ui-kit-schematics/README.fr.md | 16 +++++----------- projects/ui-kit-schematics/README.md | 16 +++++----------- projects/ui-kit/README.fr.md | 18 ++++++++++++++++++ projects/ui-kit/README.md | 18 ++++++++++++++++++ 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/projects/ui-kit-schematics/README.fr.md b/projects/ui-kit-schematics/README.fr.md index 93c79ea..0cee2cf 100644 --- a/projects/ui-kit-schematics/README.fr.md +++ b/projects/ui-kit-schematics/README.fr.md @@ -36,6 +36,7 @@ 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 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 | @@ -47,18 +48,11 @@ locales — ni dans les sources copiées, ni dans l'auto-complétion de votre é ## L'autre voie : l'utiliser comme librairie -Si vous préférez consommer les composants compilés, sans posséder aucune source : +Si vous préférez consommer les composants compilés, sans posséder aucune source, +installez **[`@4sh/ui-kit`](https://www.npmjs.com/package/@4sh/ui-kit)** et suivez +son propre README — rien n'est copié, et vous suivez les releases du kit. -```bash -npm i @4sh/ui-kit -``` - -```ts -import { UiButton } from '@4sh/ui-kit/actions/ui-button'; -``` - -Rien n'est copié, rien n'est à vous : vous suivez les releases du kit. Les deux -modes ne se combinent pas — choisissez celui qui correspond au projet. +Les deux modes ne se combinent pas : choisissez celui qui correspond au projet. --- diff --git a/projects/ui-kit-schematics/README.md b/projects/ui-kit-schematics/README.md index f723a29..ed68645 100644 --- a/projects/ui-kit-schematics/README.md +++ b/projects/ui-kit-schematics/README.md @@ -34,6 +34,7 @@ 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 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 | @@ -45,18 +46,11 @@ in the copied sources, and not in your editor's auto-import either. ## The other way: use it as a library -If you would rather consume compiled components and not own any source: +If you would rather consume compiled components and own no source, install +**[`@4sh/ui-kit`](https://www.npmjs.com/package/@4sh/ui-kit)** instead and follow +its own README — nothing is copied, and you track the kit's releases. -```bash -npm i @4sh/ui-kit -``` - -```ts -import { UiButton } from '@4sh/ui-kit/actions/ui-button'; -``` - -Nothing is copied, nothing is yours to edit — you follow the kit's releases. The -two modes do not combine: pick the one that fits the project. +The two modes do not combine: pick the one that fits the project. --- diff --git a/projects/ui-kit/README.fr.md b/projects/ui-kit/README.fr.md index 75071af..6f4dd75 100644 --- a/projects/ui-kit/README.fr.md +++ b/projects/ui-kit/README.fr.md @@ -24,6 +24,24 @@ présente dans votre application qui est utilisée (jamais un second exemplaire d'Angular) : `@angular/core`, `@angular/common`, `@angular/forms`, `@angular/router`, `@angular/cdk`, `@angular/platform-browser` et `rxjs`. +### Ou copier les sources à la place + +Ce package vous donne les composants **compilés** : vous les importez et vous +suivez les releases du kit. Si vous préférez avoir les sources *dans votre propre +dépôt*, pour les lire et les modifier — l'approche shadcn/spartan-ng — c'est +l'autre mode, et il passe par le package compagnon : + +```bash +ng add @4sh/ui-kit-schematics +``` + +Il copie les composants que vous choisissez dans `src/app/shared/`, et n'installe +délibérément **pas** `@4sh/ui-kit` : absent de `node_modules`, plus rien ne peut +importer son code compilé au lieu de vos copies. Voir +**[`@4sh/ui-kit-schematics`](https://www.npmjs.com/package/@4sh/ui-kit-schematics)**. + +Les deux modes ne se combinent pas — choisissez celui qui correspond au projet. + --- ## Import à la carte (secondary entry points) diff --git a/projects/ui-kit/README.md b/projects/ui-kit/README.md index c961caf..a2a5416 100644 --- a/projects/ui-kit/README.md +++ b/projects/ui-kit/README.md @@ -24,6 +24,24 @@ your application is the one used (never a second copy of Angular): `@angular/core`, `@angular/common`, `@angular/forms`, `@angular/router`, `@angular/cdk`, `@angular/platform-browser` and `rxjs`. +### Or copy the sources instead + +This package gives you **compiled** components: you import them and follow the +kit's releases. If you would rather have the sources *in your own repository*, to +read and edit them — the shadcn/spartan-ng approach — that is the other mode, and +it goes through the companion package: + +```bash +ng add @4sh/ui-kit-schematics +``` + +It copies the components you pick into `src/app/shared/`, and deliberately does +**not** install `@4sh/ui-kit`: absent from `node_modules`, nothing can import its +compiled code instead of your copies. See +**[`@4sh/ui-kit-schematics`](https://www.npmjs.com/package/@4sh/ui-kit-schematics)**. + +The two modes do not combine — pick the one that fits the project. + --- ## À la carte imports (secondary entry points) From aab71e5a00e3a813a7b760ec6382bca195c78b95 Mon Sep 17 00:00:00 2001 From: LBU Date: Mon, 17 Aug 2026 17:30:17 +0200 Subject: [PATCH 7/7] FSHSP-124 fix(file-upload): drop the unused UiLink declaration UiLink sat in the component's imports without ever appearing in the template: in drag mode the link is a styled inside the