diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml
index fbcd466d..4c569e9f 100644
--- a/.github/workflows/pr-checks.yml
+++ b/.github/workflows/pr-checks.yml
@@ -126,6 +126,12 @@ jobs:
- name: Build the schematics package
run: pnpm schematics:build
+ # FSHSP-228: the only place every kit import goes through the rewrite that
+ # `ng add` applies. Without it, an import the table cannot re-address fails
+ # at the consumer's install, as `type UiIconSize` did in 0.11.0 and 0.12.0.
+ - name: Schematics tests
+ run: pnpm schematics:test
+
# `postinstall` has just regenerated everything `docs:config` produces, so
# this step looks redundant. It is not — it enforces two things nothing
# else does:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a8778c76..56401d70 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,25 @@ Le format suit [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/) et le pr
## [Unreleased]
+### Fixed
+
+- **Schematics : sur un projet Angular 20, `ng add` s'arrêtait sur un ERESOLVE et laissait le
+ `package.json` à moitié migré** (FSHSP-228). Les `peerDependencies` du kit étaient recopiées
+ par-dessus les versions du projet : `@angular/core`, `common`, `forms`, `router` et
+ `platform-browser` passaient en `^22.0.0`, `compiler-cli`, `build` et `cli` restaient en 20, et
+ `@angular-devkit/build-angular@^22` faisait échouer npm.
+ - `ng add`, `add` et `update` vérifient d'abord le majeur d'Angular du projet. S'il n'est pas
+ celui du kit, ils s'arrêtent sans rien écrire et donnent les `ng update` à enchaîner.
+ - Une dépendance déjà déclarée garde sa plage. Un paquet Angular absent prend celle du
+ `@angular/core` du projet, les paquets du framework s'exigeant l'un l'autre à la version
+ exacte.
+- **Schematics : copier `ui-input-date` faisait échouer `ng add`** (FSHSP-228), depuis la
+ 0.11.0, sur « import(s) du kit non réadressé(s) … (symbole type UiIconSize) ». La réécriture
+ des imports prenait le modificateur `type` en ligne (`import { UiIcon, type UiIconSize }`)
+ pour une partie du nom. Il est ignoré pour la recherche et conservé dans l'import réécrit.
+ Les schematics ont maintenant des tests (`pnpm schematics:test`, en CI), dont un qui fait
+ passer chaque composant par la réécriture.
+
## [0.12.0] - 2026-09-25
### Added
diff --git a/package.json b/package.json
index 23a119dc..3e86f41d 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"schematics:assets": "node scripts/schematics-assets.build.mjs",
"schematics:build": "node scripts/schematics-package.build.mjs",
"schematics:pack": "pnpm schematics:build && pnpm -C dist/ui-kit-schematics pack --pack-destination ../..",
+ "schematics:test": "pnpm schematics:assets && vitest run --root projects/ui-kit-schematics",
"mcp:assets": "node scripts/mcp-assets.build.mjs",
"mcp:bundle": "node scripts/mcp-bundle.build.mjs",
"mcp:test": "vitest run --root projects/ui-kit-mcp"
diff --git a/projects/ui-kit-schematics/README.md b/projects/ui-kit-schematics/README.md
index 94486092..17b82837 100644
--- a/projects/ui-kit-schematics/README.md
+++ b/projects/ui-kit-schematics/README.md
@@ -17,6 +17,12 @@ schematics that copy them into a consuming project.
ng add @4sh/ui-kit-schematics
```
+**Requires an Angular 22 project** (the kit's `peerDependencies`: `@angular/core ^22.0.0`).
+On any other major, `ng add`, `add` and `update` stop before writing anything and print
+the `ng update` steps to run first, one major at a time. Your Angular, RxJS and CDK
+versions are never rewritten: a missing Angular package is added at your
+`@angular/core` range.
+
One command: it lays the foundation (styles, design tokens, `angular.json`), then
asks which components to copy and copies them, dependencies included. The prompt
is a checkbox list — space to pick, a for all,
diff --git a/projects/ui-kit-schematics/src/add/index.ts b/projects/ui-kit-schematics/src/add/index.ts
index d4a32a81..4dc50947 100644
--- a/projects/ui-kit-schematics/src/add/index.ts
+++ b/projects/ui-kit-schematics/src/add/index.ts
@@ -13,6 +13,7 @@ import { resolveDependencies } from '../utils/dependency-graph';
import { copyUnit } from '../utils/copy';
import { emptyManifest, readManifest, today, writeManifest } from '../utils/manifest';
import { kitVersion as readKitVersion } from '../utils/kit-manifest';
+import { checkAngularCompatibility } from '../utils/angular-version';
/**
* Sélection interactive : checkbox `@inquirer/prompts`, qui supporte déjà
@@ -29,6 +30,8 @@ async function promptComponentNames(): Promise {
export function add(options: Schema): Rule {
return async (tree: Tree, context: SchematicContext) => {
+ // Avant le prompt : des sources Angular 22 dans un projet Angular 20 ne compilent pas.
+ checkAngularCompatibility(tree, context);
const available = listComponents();
let selected: string[];
diff --git a/projects/ui-kit-schematics/src/ng-add/index.ts b/projects/ui-kit-schematics/src/ng-add/index.ts
index 8331e49e..92e826cb 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 { join } from 'node:path';
import type { Schema } from './schema';
import {
addDependency,
+ ensureDependency,
addNpmScript,
detectPackageManager,
readPackageJson,
@@ -36,6 +37,7 @@ import {
stylesFoundationDir,
} from '../utils/component-registry';
import { readKitManifestInfo } from '../utils/kit-manifest';
+import { assertAngularCompatible } from '../utils/angular-version';
import { rewriteKitPaths } from '../utils/kit-paths';
import { add } from '../add';
@@ -740,7 +742,10 @@ function applicationIndexPaths(tree: Tree): string[] {
* 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';
+ const json = readPackageJson(tree);
+ return (
+ json.dependencies?.['@angular/core'] ?? json.devDependencies?.['@angular/core'] ?? '^22.0.0'
+ );
}
/** Dépendances et scripts du Storybook du consommateur. Versions alignées sur ce dépôt. */
@@ -816,8 +821,13 @@ function addRuntimeDependencies(): Rule {
// Toutes les peerDependencies déjà déclarées par @4sh/ui-kit (@angular/cdk, rxjs…)
// deviennent des dependencies directes chez le consommateur — pas @4sh/ui-kit
// lui-même, gardé en devDependency pour piloter la CLI (voir ticket).
+ // Seulement celles qui manquent : la plage du projet reste la sienne. Un
+ // paquet Angular absent prend celle de son `@angular/core`, pas le `^22.0.0`
+ // du kit : les paquets du framework s'exigent l'un l'autre à la version
+ // exacte (voir `angularRange`).
for (const [name, version] of Object.entries(peerDependencies)) {
- addDependency(tree, name, version, 'dependencies');
+ const range = name.startsWith('@angular/') ? angularRange(tree) : version;
+ ensureDependency(tree, name, range, 'dependencies');
}
// FontAwesome n'est pas une peerDependency du kit (elle n'est pas importée
// par le TypeScript : `ui-icon` ne pose que des classes CSS), mais la
@@ -1138,6 +1148,9 @@ export function ngAdd(options: Schema): Rule {
const withMcp = !(options.skipMcp ?? false);
const foundation = [
+ // Avant tout le reste, prompts compris : un projet d'un autre majeur
+ // d'Angular est refusé sans que rien ne soit écrit ni demandé.
+ assertAngularCompatible(),
copyStylesFoundationRule(),
createStyleScaffolds(),
// Avant `updateAngularJson`, qui déclare `src/assets` au builder : la
diff --git a/projects/ui-kit-schematics/src/update/index.ts b/projects/ui-kit-schematics/src/update/index.ts
index e36bf313..97de549e 100644
--- a/projects/ui-kit-schematics/src/update/index.ts
+++ b/projects/ui-kit-schematics/src/update/index.ts
@@ -15,6 +15,7 @@ import { findUnit } from '../utils/component-registry';
import { renderUnitFiles } from '../utils/copy';
import { readManifest, today, writeManifest } from '../utils/manifest';
import { kitVersion as readKitVersion } from '../utils/kit-manifest';
+import { checkAngularCompatibility } from '../utils/angular-version';
type Action = 'apply' | 'skip' | 'view-diff';
@@ -88,13 +89,15 @@ function logUncoveredConcerns(context: SchematicContext): void {
`d'une version plus ancienne du kit, \`ng add @4sh/ui-kit-schematics ` +
`--skip-components\` réapplique ce qui peut l'être sans risque : vos composants, ` +
`votre \`.mcp.json\`, votre config Prettier et vos styles retouchés sont ` +
- `préservés. Seule réserve mesurée : une dépendance que vous auriez épinglée peut ` +
- `y être ré-élargie vers la plage du kit.`,
+ `préservés. Seule réserve mesurée : une dépendance d'outillage que vous auriez épinglée ` +
+ `(Storybook, Prettier…) peut y être ré-élargie vers la plage du kit ; Angular et ` +
+ `RxJS gardent la vôtre.`,
);
}
export function update(options: Schema): Rule {
return async (tree: Tree, context: SchematicContext) => {
+ checkAngularCompatibility(tree, context);
const manifest = readManifest(tree);
if (!manifest) {
throw new SchematicsException(
diff --git a/projects/ui-kit-schematics/src/utils/angular-version.spec.ts b/projects/ui-kit-schematics/src/utils/angular-version.spec.ts
new file mode 100644
index 00000000..215957e7
--- /dev/null
+++ b/projects/ui-kit-schematics/src/utils/angular-version.spec.ts
@@ -0,0 +1,105 @@
+import { HostTree, SchematicsException } from '@angular-devkit/schematics';
+import type { SchematicContext } from '@angular-devkit/schematics';
+import { describe, expect, it, vi } from 'vitest';
+import { angularMismatch, checkAngularCompatibility, majorOf } from './angular-version';
+import { readKitManifestInfo } from './kit-manifest';
+import { ensureDependency, readPackageJson } from './package-json';
+
+function projectTree(dependencies: Record): HostTree {
+ const tree = new HostTree();
+ tree.create('/package.json', JSON.stringify({ name: 'test', dependencies }));
+ return tree;
+}
+
+function fakeContext() {
+ const warn = vi.fn();
+ return { context: { logger: { warn } } as unknown as SchematicContext, warn };
+}
+
+describe('majorOf', () => {
+ it.each([
+ ['^20.3.0', 20],
+ ['~20.3.0', 20],
+ ['20.3.0', 20],
+ ['>=20.0.0 <23', 20],
+ ['20.x', 20],
+ ['v22', 22],
+ ['22.0.0-next.1', 22],
+ ['^22.0.0 || ^23.0.0', 22],
+ ])('%s → %i', (range, major) => {
+ expect(majorOf(range)).toBe(major);
+ });
+
+ it.each(['latest', '*', 'workspace:*', 'file:../core', 'npm:@angular/core@22', undefined])(
+ '%s → illisible',
+ (range) => {
+ expect(majorOf(range)).toBeUndefined();
+ },
+ );
+});
+
+describe('angularMismatch', () => {
+ it("laisse passer le majeur du kit, quelle qu'en soit la mineure", () => {
+ expect(angularMismatch('^22.2.0', '^22.0.0', '0.12.1')).toBeUndefined();
+ });
+
+ it('donne les montées de majeur une à une pour un projet en retard', () => {
+ const message = angularMismatch('^20.3.0', '^22.0.0', '0.12.1');
+ expect(message).toContain('Angular 22');
+ expect(message).toContain('Angular 20');
+ expect(message).toContain('ng update @angular/core@21 @angular/cli@21');
+ expect(message).toContain('ng update @angular/core@22 @angular/cli@22');
+ expect(message).toContain("n'a rien modifié");
+ });
+
+ it('refuse un majeur que le kit ne connaît pas encore, sans proposer de descendre', () => {
+ const message = angularMismatch('^23.0.0', '^22.0.0', '0.12.1');
+ expect(message).toContain('ne connaît pas encore Angular 23');
+ expect(message).not.toContain('ng update');
+ });
+});
+
+describe('checkAngularCompatibility', () => {
+ const kitRange = readKitManifestInfo().peerDependencies['@angular/core'];
+ const kitMajor = majorOf(kitRange)!;
+
+ it('refuse un projet Angular 20 (ERESOLVE du ng add de la 0.12.0)', () => {
+ const { context } = fakeContext();
+ expect(() =>
+ checkAngularCompatibility(projectTree({ '@angular/core': '^20.3.0' }), context),
+ ).toThrow(SchematicsException);
+ });
+
+ it('accepte un projet du majeur du kit', () => {
+ const { context, warn } = fakeContext();
+ checkAngularCompatibility(projectTree({ '@angular/core': `^${kitMajor}.1.0` }), context);
+ expect(warn).not.toHaveBeenCalled();
+ });
+
+ it('avertit sans refuser quand la plage ne se lit pas', () => {
+ const { context, warn } = fakeContext();
+ checkAngularCompatibility(projectTree({ '@angular/core': 'latest' }), context);
+ expect(warn).toHaveBeenCalledOnce();
+ });
+});
+
+describe('ensureDependency', () => {
+ it("ne remplace jamais la plage d'un paquet déjà déclaré", () => {
+ const tree = projectTree({ '@angular/core': '^20.3.0' });
+ expect(ensureDependency(tree, '@angular/core', '^22.0.0')).toBe(false);
+ expect(readPackageJson(tree).dependencies?.['@angular/core']).toBe('^20.3.0');
+ });
+
+ it('ne le recopie pas en dependencies quand il est en devDependencies', () => {
+ const tree = new HostTree();
+ tree.create('/package.json', JSON.stringify({ devDependencies: { rxjs: '~7.8.0' } }));
+ expect(ensureDependency(tree, 'rxjs', '^7.8.0')).toBe(false);
+ expect(readPackageJson(tree).dependencies).toBeUndefined();
+ });
+
+ it('ajoute un paquet absent', () => {
+ const tree = projectTree({});
+ expect(ensureDependency(tree, '@angular/cdk', '^22.1.0')).toBe(true);
+ expect(readPackageJson(tree).dependencies?.['@angular/cdk']).toBe('^22.1.0');
+ });
+});
diff --git a/projects/ui-kit-schematics/src/utils/angular-version.ts b/projects/ui-kit-schematics/src/utils/angular-version.ts
new file mode 100644
index 00000000..3fb9ae13
--- /dev/null
+++ b/projects/ui-kit-schematics/src/utils/angular-version.ts
@@ -0,0 +1,107 @@
+/**
+ * angular-version — refuser un projet dont le majeur d'Angular n'est pas celui du kit.
+ *
+ * Les sources copiées et les dépendances que `ng add` inscrit visent le majeur
+ * des `peerDependencies` du kit. Posées dans un projet d'un autre majeur, elles
+ * ne compilent pas, et l'install s'arrête sur un ERESOLVE qui ne dit pas d'où il
+ * vient (Angular 20 : `@angular-devkit/build-angular@^22` contre le
+ * `compiler-cli@^20` du projet). Monter Angular d'un majeur est le travail
+ * d'`ng update`, avec ses migrations : un schematic de composants ne le fait pas
+ * à moitié. On s'arrête donc AVANT toute écriture, en disant quoi faire.
+ */
+import type { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
+import { SchematicsException } from '@angular-devkit/schematics';
+import { readKitManifestInfo } from './kit-manifest';
+import { readPackageJson } from './package-json';
+
+/**
+ * Majeur plancher d'une plage npm : `^20.3.0`, `~20.3.0`, `20.3.0`, `>=20.0.0`,
+ * `20.x`, `v20` → 20. `undefined` pour ce qui ne se lit pas ainsi (`latest`,
+ * `*`, `file:…`, `workspace:*`, `npm:…`) : l'appelant ne devine pas.
+ */
+export function majorOf(range: string | undefined): number | undefined {
+ const match = /^\s*(?:[\^~]|>=?)?\s*v?(\d+)(?:[.\sx*]|$)/.exec(range ?? '');
+ return match ? Number(match[1]) : undefined;
+}
+
+/** Plage de `@angular/core` du projet, dépendances puis devDependencies. */
+function projectCoreRange(tree: Tree): string | undefined {
+ const json = readPackageJson(tree);
+ return json.dependencies?.['@angular/core'] ?? json.devDependencies?.['@angular/core'];
+}
+
+/** Version installée de `@angular/core`, quand la plage déclarée ne se lit pas. */
+function installedCoreVersion(tree: Tree): string | undefined {
+ const buffer = tree.read('/node_modules/@angular/core/package.json');
+ if (!buffer) return undefined;
+ try {
+ return JSON.parse(buffer.toString('utf8')).version;
+ } catch {
+ return undefined;
+ }
+}
+
+/**
+ * Message d'échec, ou `undefined` si le projet est compatible ou illisible.
+ * Séparé de la règle pour être testé sans arbre.
+ */
+export function angularMismatch(
+ projectRange: string | undefined,
+ kitRange: string,
+ kitVersion: string,
+): string | undefined {
+ const project = majorOf(projectRange);
+ const kit = majorOf(kitRange);
+ if (project === undefined || kit === undefined || project === kit) return undefined;
+
+ const found =
+ `@4sh/ui-kit ${kitVersion} demande Angular ${kit} (@angular/core ${kitRange}), ` +
+ `ce projet est en Angular ${project} (@angular/core ${projectRange}).`;
+ const untouched =
+ "Le schematic n'a rien modifié. Seul `@4sh/ui-kit-schematics`, ajouté par `ng add`, " +
+ 'est à retirer du package.json si vous en restez là.';
+ if (project > kit) {
+ return `${found}\nCette version du kit ne connaît pas encore Angular ${project}.\n${untouched}`;
+ }
+ const steps = Array.from({ length: kit - project }, (_, i) => project + i + 1)
+ .map((major) => ` ng update @angular/core@${major} @angular/cli@${major}`)
+ .join('\n');
+ return (
+ `${found}\nMettre le projet à niveau d'abord, un majeur à la fois ` +
+ `(https://angular.dev/update-guide) :\n${steps}\n` +
+ `puis relancer \`ng add @4sh/ui-kit-schematics\`.\n${untouched}`
+ );
+}
+
+/**
+ * À appeler avant toute écriture ET avant tout prompt. Ne lit que
+ * `package.json` (et, à défaut, la version installée) : un projet dont la plage
+ * ne se lit pas passe, avec un avertissement, plutôt que d'être refusé sur une
+ * supposition.
+ */
+export function checkAngularCompatibility(tree: Tree, context: SchematicContext): void {
+ const { version, peerDependencies } = readKitManifestInfo();
+ const kitRange = peerDependencies['@angular/core'];
+ if (!kitRange) return;
+
+ const declared = projectCoreRange(tree);
+ const range = majorOf(declared) !== undefined ? declared : installedCoreVersion(tree);
+ if (majorOf(range) === undefined) {
+ context.logger.warn(
+ `Version d'Angular du projet illisible (@angular/core « ${declared ?? 'absent'} ») : ` +
+ `le kit demande ${kitRange}, vérification sautée.`,
+ );
+ return;
+ }
+
+ const mismatch = angularMismatch(range, kitRange, version);
+ if (mismatch) throw new SchematicsException(mismatch);
+}
+
+/** {@link checkAngularCompatibility} en règle, pour la tête d'une chaîne. */
+export function assertAngularCompatible(): Rule {
+ return (tree, context) => {
+ checkAngularCompatibility(tree, context);
+ return tree;
+ };
+}
diff --git a/projects/ui-kit-schematics/src/utils/copy.spec.ts b/projects/ui-kit-schematics/src/utils/copy.spec.ts
new file mode 100644
index 00000000..b5228238
--- /dev/null
+++ b/projects/ui-kit-schematics/src/utils/copy.spec.ts
@@ -0,0 +1,24 @@
+import { describe, expect, it } from 'vitest';
+import { listComponents, listSharedBases } from './component-registry';
+import { renderUnitFiles } from './copy';
+
+// Le seul endroit où TOUS les imports du kit passent par la réécriture. Sans
+// lui, un import que la table ne sait pas réadresser n'échoue que chez le
+// consommateur, au `ng add` : c'est ainsi que `type UiIconSize` (ui-input-date)
+// est parti en 0.11.0 et 0.12.0.
+describe('renderUnitFiles, sur chaque unité du kit', () => {
+ const units = [...listComponents(), ...listSharedBases()];
+
+ it('trouve les unités (assets/ généré)', () => {
+ // Une liste vide ferait passer la suite sans rien vérifier.
+ expect(listComponents().length).toBeGreaterThan(40);
+ expect(listSharedBases().length).toBeGreaterThan(0);
+ });
+
+ it.each(units.map((unit) => [unit.name, unit] as const))(
+ '%s : aucun import du kit non réadressé',
+ (_name, unit) => {
+ expect(() => renderUnitFiles(unit, '0.0.0-test', { withStorybook: true })).not.toThrow();
+ },
+ );
+});
diff --git a/projects/ui-kit-schematics/src/utils/package-json.ts b/projects/ui-kit-schematics/src/utils/package-json.ts
index ad86fcae..de1daf34 100644
--- a/projects/ui-kit-schematics/src/utils/package-json.ts
+++ b/projects/ui-kit-schematics/src/utils/package-json.ts
@@ -30,6 +30,24 @@ export function addDependency(
writePackageJson(tree, json);
}
+/**
+ * Comme {@link addDependency}, mais n'écrit rien si le paquet est déjà déclaré,
+ * en dependencies comme en devDependencies. Pour ce que le projet possède déjà
+ * (Angular, RxJS), sa plage est la bonne : la remplacer par celle du kit a
+ * monté la moitié d'Angular d'un majeur dans un projet Angular 20.
+ */
+export function ensureDependency(
+ tree: Tree,
+ name: string,
+ version: string,
+ target: 'dependencies' | 'devDependencies' = 'dependencies',
+): boolean {
+ const json = readPackageJson(tree);
+ if (json.dependencies?.[name] ?? json.devDependencies?.[name]) return false;
+ addDependency(tree, name, version, target);
+ return true;
+}
+
export function addNpmScript(tree: Tree, name: string, command: string): void {
const json = readPackageJson(tree);
if (json.scripts?.[name]) return; // ne jamais écraser un script déjà défini par le consommateur
diff --git a/projects/ui-kit-schematics/src/utils/rewrite-imports.spec.ts b/projects/ui-kit-schematics/src/utils/rewrite-imports.spec.ts
new file mode 100644
index 00000000..e3ed9977
--- /dev/null
+++ b/projects/ui-kit-schematics/src/utils/rewrite-imports.spec.ts
@@ -0,0 +1,46 @@
+import { describe, expect, it } from 'vitest';
+import { rewriteKitImports, symbolName } from './rewrite-imports';
+
+const FROM = 'src/app/shared/components/ui/forms/ui-input-date/ui-input-date.ts';
+
+describe('symbolName', () => {
+ it.each([
+ ['UiIcon', 'UiIcon'],
+ ['UiIcon as Icon', 'UiIcon'],
+ ['type UiIconSize', 'UiIconSize'],
+ [' type UiIconSize as Size ', 'UiIconSize'],
+ ])('%s → %s', (clause, expected) => {
+ expect(symbolName(clause)).toBe(expected);
+ });
+});
+
+describe('rewriteKitImports', () => {
+ it('réadresse un symbole importé avec le modificateur `type` en ligne', () => {
+ const { content, unresolved } = rewriteKitImports(
+ "import { UiIcon, type UiIconSize } from '@4sh/ui-kit/base/ui-icon';",
+ FROM,
+ );
+ expect(unresolved).toEqual([]);
+ expect(content).not.toContain('@4sh/ui-kit');
+ // Le modificateur survit à la réécriture : l'import reste valide pour
+ // `verbatimModuleSyntax` / `isolatedModules`.
+ expect(content).toMatch(/import \{ UiIcon, type UiIconSize \} from '\.\.?\/[^']*ui-icon';/);
+ });
+
+ it('garde un import `type` complet en `import type`', () => {
+ const { content, unresolved } = rewriteKitImports(
+ "import type { UiIconSize } from '@4sh/ui-kit/base/ui-icon';",
+ FROM,
+ );
+ expect(unresolved).toEqual([]);
+ expect(content).toMatch(/^import type \{ UiIconSize \} from '/);
+ });
+
+ it('signale un symbole que le kit n’exporte pas, sans deviner', () => {
+ const { unresolved } = rewriteKitImports(
+ "import { NotAKitSymbol } from '@4sh/ui-kit/base/ui-icon';",
+ FROM,
+ );
+ expect(unresolved).toEqual(['base/ui-icon (symbole NotAKitSymbol)']);
+ });
+});
diff --git a/projects/ui-kit-schematics/src/utils/rewrite-imports.ts b/projects/ui-kit-schematics/src/utils/rewrite-imports.ts
index 862d711a..431433f5 100644
--- a/projects/ui-kit-schematics/src/utils/rewrite-imports.ts
+++ b/projects/ui-kit-schematics/src/utils/rewrite-imports.ts
@@ -53,10 +53,15 @@ function relativeSpecifier(fromFileTargetPath: string, toTargetPath: string): st
return rel.startsWith('.') ? rel : `./${rel}`;
}
-/** Un symbole importé, avec son éventuel alias : `UiIcon`, `UiIcon as Icon`. */
-function symbolName(clause: string): string {
+/**
+ * Un symbole importé, avec son éventuel alias et son modificateur `type`
+ * en ligne : `UiIcon`, `UiIcon as Icon`, `type UiIconSize`. Le modificateur est
+ * retiré pour la recherche dans la table, et conservé dans l'import réécrit.
+ */
+export function symbolName(clause: string): string {
return clause
.trim()
+ .replace(/^type\s+/, '')
.split(/\s+as\s+/)[0]
.trim();
}
diff --git a/projects/ui-kit-schematics/vitest.config.ts b/projects/ui-kit-schematics/vitest.config.ts
new file mode 100644
index 00000000..693aea8d
--- /dev/null
+++ b/projects/ui-kit-schematics/vitest.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vitest/config';
+
+// Package Node pur, comme projects/ui-kit-mcp : Vitest tourne directement sur
+// les `.spec.ts` (exclus du build par tsconfig.json). Les specs lisent `assets/`,
+// produit par `pnpm schematics:assets` : lancer `pnpm schematics:test`, qui le
+// régénère d'abord.
+export default defineConfig({
+ test: {
+ environment: 'node',
+ include: ['src/**/*.spec.ts'],
+ passWithNoTests: false,
+ },
+});
diff --git a/projects/ui-kit/README.md b/projects/ui-kit/README.md
index a74387f0..83f08686 100644
--- a/projects/ui-kit/README.md
+++ b/projects/ui-kit/README.md
@@ -59,6 +59,11 @@ Published on the **public npm registry**, under the **`4sh`** organization:
These are declared as `peerDependencies`: the version already present in your
application is the one used, never a second copy of Angular.
+On an older Angular, **npm refuses** the install (`ERESOLVE`), but **pnpm only warns**
+and installs anyway (`✕ unmet peer @angular/core@^22.0.0`): that line means the project
+must be upgraded first (`ng update @angular/core@22 @angular/cli@22`, one major at a
+time), not that the kit is ready to use.
+
### Or copy the sources instead
This package gives you **compiled** components: you import them and follow the