Compare commits

...

6 commits

Author SHA1 Message Date
38aa956fde Merge pull request 'Ajouter @lightweight-sdk/service-registry (packages/common)' (#2) from feat/service-registry-package into main
All checks were successful
Release / release (push) Successful in 27s
Reviewed-on: #2
2026-09-18 20:56:44 +02:00
544614add7 Simplifier : retirer le generique de @Service, ajouter services
All checks were successful
CI / build-lint-test (pull_request) Successful in 34s
- @Service(uid) n'a plus de parametre generique : un decorateur de
  propriete legacy ne peut de toute facon pas changer le type du champ
  qu'il decore, seule son annotation compte. Le generique n'apportait
  rien.
- Nouvelle constante exportee services = ServiceRepository.getInstance(),
  instance prete a l'emploi : services.get<T>(uid), services.has(uid),
  services.register(uid, Classe). C'est desormais le point d'entree
  recommande plutot que ServiceRepository.getInstance() directement.
- ServiceRepository.resolve() renomme en get() pour matcher
  services.get<T>(uid).
- resetForTesting() vide la map en place (Map.clear()) au lieu de
  jeter l'instance singleton, pour que la constante services capturee
  au chargement du module reste valide apres un reset dans les tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 14:31:42 +02:00
7d11dd1426 uid fourni par le developpeur au lieu d'etre auto-genere
All checks were successful
CI / build-lint-test (pull_request) Successful in 37s
@RegisterService(uid) et @Service<T>(uid) prennent desormais l'uid en
parametre explicite, choisi par l'appelant, plutot que de le generer
automatiquement (crypto.randomUUID) et de resoudre par reference de
classe. ServiceRepository simplifie en consequence : une seule map
uid -> instance (plus de WeakMap constructeur -> uid, resolveClass/
getUidForClass/hasClass supprimes, devenus inutiles).

@Service ne peut plus inferer le type depuis une reference de classe :
a annoter la propriete ou a typer explicitement (@Service<T>(uid)).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 14:22:06 +02:00
f0f57f170c Corriger pnpm-lock.yaml : entree manquante pour service-registry
All checks were successful
CI / build-lint-test (pull_request) Successful in 34s
pnpm 12.4.2 n'ecrivait pas d'entree importers pour
packages/common/service-registry dans le lockfile, meme apres
suppression complete du lockfile et du cache d'etat pnpm puis
reinstallation (pnpm ls -r reconnaissait pourtant bien le package).
CI echouait avec --frozen-lockfile (erreur has no importers[...] entry),
alors que le pnpm install local sans --frozen-lockfile ne se
plaignait de rien.

Cause probable : ce package ne declarait aucune dependency ni
devDependency propre. Remede : declarer explicitement quelques
devDependencies (typescript, vitest, @types/node via catalog:), meme
deja hoistees depuis la racine. create-package.mjs generera desormais
ces devDependencies par defaut pour tout nouveau package, et le
troubleshooting est documente dans spec/ci-cd.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 14:06:19 +02:00
10bc209038 Ajouter @lightweight-sdk/service-registry (packages/common)
Some checks failed
CI / build-lint-test (pull_request) Failing after 29s
Registre de services en singleton avec injection par decorateurs :
- ServiceRepository : singleton central, uid genere automatiquement
  (crypto.randomUUID) a l'enregistrement, une seule instance partagee
  par service.
- @RegisterService() : decorateur de classe, instancie et enregistre
  automatiquement la classe decoree (constructeur sans argument).
- @Service(target) : decorateur de propriete, injection paresseuse et
  memoisee par reference de classe (resolution au premier acces, pas
  a la decoration - insensible a l'ordre d'evaluation des modules).

Fix connexe dans tsconfig.base.json : useDefineForClassFields desactive
(target ES2022 l'active par defaut), sinon un champ de classe sans
initialiseur est reinitialise a undefined sur l'instance a la
construction et masque l'accesseur pose sur le prototype par un
decorateur de propriete - touche @Service ici et @LinkToDom dans
web-component. Documente dans spec/architecture.md.

Fix connexe dans scripts/create-package.mjs : execFileSync('pnpm', ...)
ne resout pas le shim .cmd de pnpm sous Windows sans shell: true
(trouve en re-testant le script sur ce package).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 13:59:58 +02:00
cb8fbb4b3b Merge pull request 'Bootstrap monorepo pnpm + migration de web-component' (#1) from feat/monorepo-bootstrap into main
Some checks failed
Release / release (push) Failing after 35s
2026-09-18 11:36:45 +02:00
20 changed files with 563 additions and 4 deletions

View file

@ -0,0 +1,5 @@
---
"@lightweight-sdk/service-registry": minor
---
Ajout du package `@lightweight-sdk/service-registry` : registre de services en singleton avec `@RegisterService()` (enregistrement automatique à la décoration de classe) et `@Service(target)` (injection paresseuse et mémoïsée par référence de classe sur un membre).

View file

@ -0,0 +1,59 @@
# @lightweight-sdk/service-registry
Registre de services en singleton, avec enregistrement par décorateur et résolution directe. Package `common` : isomorphe, aucune dépendance DOM ou Node-only.
## Installation
```bash
pnpm add @lightweight-sdk/service-registry
```
## Usage
```ts
import { RegisterService, Service, services } from '@lightweight-sdk/service-registry';
export const LOGGER_UID = 'logger';
@RegisterService(LOGGER_UID)
class LoggerService {
public log(message: string): void {
console.log(message);
}
}
// Injection déclarative sur une propriété
class ReportGenerator {
@Service(LOGGER_UID)
private readonly logger!: LoggerService;
public run(): void {
this.logger.log('generating report');
}
}
// Résolution directe, n'importe où
const logger = services.get<LoggerService>(LOGGER_UID);
logger.log('hello');
```
## Concepts
- **`services`** — constante globale, instance prête à l'emploi du `ServiceRepository`. C'est le point d'entrée à utiliser partout : `services.get<T>(uid)`, `services.has(uid)`, `services.register(uid, MaClasse)`.
- **`ServiceRepository`** — la classe singleton derrière `services` (accès direct rarement nécessaire, exportée pour le typage et les tests).
- **`@RegisterService(uid)`** — décorateur de classe. Instancie la classe décorée (constructeur sans argument obligatoire) et l'enregistre sous `uid`. **C'est au développeur de choisir `uid` et de garantir son unicité** — la registry refuse tout doublon.
- **`@Service(uid)`** — décorateur de propriété. Injecte l'instance enregistrée sous `uid` dans le membre décoré. Pas de typage générique sur le décorateur : un décorateur de propriété ne peut pas changer le type du champ qu'il décore, donc c'est l'annotation du champ (`private readonly logger!: LoggerService;`) qui le type, comme d'habitude. La résolution est **paresseuse** (au premier accès à la propriété, pas à la décoration) : peu importe que la classe consommatrice soit évaluée avant ou après l'enregistrement du service. Après le premier accès, la propriété est remplacée par la valeur résolue (pas de nouvelle recherche ensuite).
## Contraintes
- Une classe décorée par `@RegisterService(uid)` doit avoir un constructeur sans argument obligatoire — ses propres dépendances doivent elles-mêmes être injectées via `@Service` ou `services.get`, pas passées au constructeur.
- `uid` doit être unique dans toute l'application — exporter une constante partagée (comme `LOGGER_UID` ci-dessus) entre le point d'enregistrement et les points d'injection évite les fautes de frappe.
- `experimentalDecorators: true` doit être activé côté consommateur (déjà le cas dans ce monorepo via `tsconfig.base.json`).
## API
- `services` — instance singleton prête à l'emploi (`get<T>(uid)`, `has(uid)`, `register(uid, target)`).
- `RegisterService(uid)` — décorateur de classe.
- `Service(uid)` — décorateur de propriété.
- `ServiceRepository` — la classe singleton (`getInstance()`, `register(uid, target)`, `get<T>(uid)`, `has(uid)`). `resetForTesting()` est réservé aux tests.
- `ServiceConstructor<T>`, `ServiceUid` — types associés.

View file

@ -0,0 +1,28 @@
{
"name": "@lightweight-sdk/service-registry",
"version": "0.0.1",
"description": "Registre de services en singleton avec injection par décorateurs (@RegisterService, @Service).",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">=22"
},
"publishConfig": {
"access": "restricted",
"registry": "https://git.dev.kyuno.fr/api/packages/kyuno/npm/"
},
"typedocOptions": {
"entryPoints": [
"src/index.ts"
],
"readme": "README.md"
},
"devDependencies": {
"@types/node": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
}
}

View file

@ -0,0 +1,35 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { RegisterService, Service, ServiceRepository, services } from './index';
beforeEach(() => {
ServiceRepository.resetForTesting();
});
describe('public API (barrel export)', () => {
it('registers and injects a service end to end through the public exports', () => {
@RegisterService('clock')
class ClockService {
public now(): number {
return 1234;
}
}
class Consumer {
@Service('clock')
public readonly clock!: ClockService;
}
expect(new Consumer().clock.now()).toBe(1234);
});
it('resolves a service directly through the services constant', () => {
@RegisterService('clock')
class ClockService {
public now(): number {
return 5678;
}
}
expect(services.get<ClockService>('clock').now()).toBe(5678);
});
});

View file

@ -0,0 +1,4 @@
export { RegisterService } from './register-service.decorator';
export { Service } from './service.decorator';
export { ServiceRepository, services } from './service-repository';
export { ServiceConstructor, ServiceUid } from './types';

View file

@ -0,0 +1,40 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { RegisterService } from './register-service.decorator';
import { ServiceRepository, services } from './service-repository';
beforeEach(() => {
ServiceRepository.resetForTesting();
});
describe('RegisterService', () => {
it('registers the decorated class under the given uid', () => {
@RegisterService('greeter')
class GreeterService {
public greet(name: string): string {
return `Hello, ${name}!`;
}
}
expect(services.has('greeter')).toBe(true);
expect(services.get<GreeterService>('greeter')).toBeInstanceOf(GreeterService);
});
it('shares a single instance across every resolution', () => {
@RegisterService('state')
class StateService {
public value = 0;
}
services.get<StateService>('state').value = 42;
expect(services.get<StateService>('state').value).toBe(42);
});
it('rethrows if the uid is already registered', () => {
class ServiceA {}
class ServiceB {}
RegisterService('duplicate')(ServiceA);
expect(() => RegisterService('duplicate')(ServiceB)).toThrow(/already registered/);
});
});

View file

@ -0,0 +1,37 @@
import { services } from './service-repository';
import type { ServiceConstructor, ServiceUid } from './types';
const LOG_PREFIX = '[lightweight-sdk:service-registry]';
/**
* Class decorator that instantiates the decorated class and registers the
* resulting instance in {@link services} under `uid`, making it resolvable
* from anywhere via `services.get(uid)` or {@link Service}. The caller picks
* `uid` and is responsible for keeping it unique across the application.
* The decorated class must have a no-argument constructor — any
* dependencies it needs should themselves be injected with {@link Service}
* rather than passed as constructor parameters.
* @param uid the unique identifier this service will be registered and resolved under
* @returns a class decorator
* @example
* ```ts
* export const LOGGER_UID = 'logger';
*
* ⁣@RegisterService(LOGGER_UID)
* export class LoggerService {
* public log(message: string): void {
* console.log(message);
* }
* }
* ```
*/
export function RegisterService(uid: ServiceUid): <T>(target: ServiceConstructor<T>) => void {
return <T>(target: ServiceConstructor<T>): void => {
try {
services.register(uid, target);
} catch (error) {
console.error(`${LOG_PREFIX} Failed to register service "${uid}" (${target.name}).`, error);
throw error;
}
};
}

View file

@ -0,0 +1,50 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { ServiceRepository, services } from './service-repository';
class Counter {
public value = 0;
}
beforeEach(() => {
ServiceRepository.resetForTesting();
});
describe('ServiceRepository', () => {
it('exposes a single instance via getInstance, matching the services constant', () => {
expect(ServiceRepository.getInstance()).toBe(ServiceRepository.getInstance());
expect(ServiceRepository.getInstance()).toBe(services);
});
it('registers a class and resolves the same instance by its uid', () => {
services.register('counter', Counter);
const first = services.get<Counter>('counter');
const second = services.get<Counter>('counter');
expect(first).toBeInstanceOf(Counter);
expect(first).toBe(second);
});
it('reports whether a uid is registered', () => {
expect(services.has('counter')).toBe(false);
services.register('counter', Counter);
expect(services.has('counter')).toBe(true);
});
it('throws when registering the same uid twice', () => {
services.register('counter', Counter);
expect(() => services.register('counter', Counter)).toThrow(/already registered/);
});
it('throws when resolving a uid that was never registered', () => {
expect(() => services.get('does-not-exist')).toThrow(/No service registered/);
});
it('clears registered services after resetForTesting, without discarding the singleton', () => {
services.register('counter', Counter);
ServiceRepository.resetForTesting();
expect(services.has('counter')).toBe(false);
expect(ServiceRepository.getInstance()).toBe(services);
});
});

View file

@ -0,0 +1,99 @@
import type { ServiceConstructor, ServiceUid } from './types';
const LOG_PREFIX = '[lightweight-sdk:service-registry]';
/**
* Central singleton registry that holds the single shared instance of every
* service registered via {@link RegisterService}, keyed by the uid the
* developer assigned at registration time. Prefer the ready-to-use
* {@link services} constant over calling {@link ServiceRepository.getInstance}
* directly.
*/
export class ServiceRepository {
private static instance: ServiceRepository | undefined;
private instances = new Map<ServiceUid, unknown>();
/** Private to enforce the singleton pattern — use {@link ServiceRepository.getInstance} or {@link services}. */
private constructor() {}
/**
* Returns the single shared instance of the repository, creating it on first access.
* @returns the singleton {@link ServiceRepository}
*/
public static getInstance(): ServiceRepository {
if (ServiceRepository.instance === undefined) {
ServiceRepository.instance = new ServiceRepository();
}
return ServiceRepository.instance;
}
/**
* Clears every registered service. The singleton instance itself is kept
* (not discarded), so the {@link services} constant stays valid across
* resets. Intended for resetting state between test cases — application
* code should not call this.
*/
public static resetForTesting(): void {
ServiceRepository.getInstance().instances.clear();
}
/**
* Instantiates `target` (its constructor must take no arguments) and
* registers the resulting instance under `uid`. Called by
* {@link RegisterService} — most callers should use that decorator
* instead of calling this method directly.
* @param uid the unique identifier the caller assigns to this service
* @param target the service class to instantiate and register
* @throws {Error} if `uid` is already registered
*/
public register<T>(uid: ServiceUid, target: ServiceConstructor<T>): void {
if (this.instances.has(uid)) {
throw new Error(`Service uid "${uid}" is already registered.`);
}
const instance = new target();
this.instances.set(uid, instance);
console.debug(`${LOG_PREFIX} Registered service "${uid}" (${target.name}).`);
}
/**
* Resolves the instance registered under `uid`.
* @param uid the service's unique identifier, as passed to {@link RegisterService}
* @returns the instance registered under `uid`
* @throws {Error} if no service is registered under `uid`
* @example
* ```ts
* const logger = services.get<LoggerService>(LOGGER_UID);
* ```
*/
public get<T>(uid: ServiceUid): T {
if (!this.instances.has(uid)) {
throw new Error(`No service registered with uid "${uid}". Did you forget @RegisterService?`);
}
return this.instances.get(uid) as T;
}
/**
* Checks whether a service is currently registered under `uid`.
* @param uid the unique identifier to check
* @returns `true` if a service is registered under `uid`
*/
public has(uid: ServiceUid): boolean {
return this.instances.has(uid);
}
}
/**
* Ready-to-use singleton instance of {@link ServiceRepository}. Import this
* wherever a service needs to be registered or resolved, instead of calling
* {@link ServiceRepository.getInstance} directly.
* @example
* ```ts
* import { services } from '@lightweight-sdk/service-registry';
*
* const logger = services.get<LoggerService>(LOGGER_UID);
* ```
*/
export const services = ServiceRepository.getInstance();

View file

@ -0,0 +1,79 @@
import { beforeEach, describe, expect, it } from 'vitest';
import { RegisterService } from './register-service.decorator';
import { Service } from './service.decorator';
import { ServiceRepository, services } from './service-repository';
beforeEach(() => {
ServiceRepository.resetForTesting();
});
describe('Service', () => {
it('injects the instance registered under the given uid', () => {
@RegisterService('logger')
class LoggerService {
public readonly messages: string[] = [];
public log(message: string): void {
this.messages.push(message);
}
}
class ReportGenerator {
@Service('logger')
public readonly logger!: LoggerService;
}
const generator = new ReportGenerator();
generator.logger.log('hello');
expect(generator.logger).toBeInstanceOf(LoggerService);
expect(services.get<LoggerService>('logger').messages).toEqual(['hello']);
});
it('resolves lazily: decoration does not require the uid to be registered yet', () => {
class LateService {
public readonly ok = true;
}
class Consumer {
@Service('late')
public readonly svc!: LateService;
}
// Consumer (and its @Service decoration) already ran above, yet "late"
// is only registered now — proves decoration itself never required it.
RegisterService('late')(LateService);
expect(new Consumer().svc.ok).toBe(true);
});
it('memoizes the resolved value on the instance after the first access', () => {
@RegisterService('logger')
class LoggerService {
public log(_message: string): void {}
}
class Consumer {
@Service('logger')
public readonly logger!: LoggerService;
}
const consumer = new Consumer();
expect(Object.getOwnPropertyDescriptor(consumer, 'logger')).toBeUndefined();
void consumer.logger;
const descriptor = Object.getOwnPropertyDescriptor(consumer, 'logger');
expect(descriptor?.get).toBeUndefined();
expect(descriptor?.value).toBeInstanceOf(LoggerService);
});
it('throws when the uid was never registered', () => {
class Consumer {
@Service('unregistered')
public readonly dep!: unknown;
}
expect(() => new Consumer().dep).toThrow(/No service registered/);
});
});

View file

@ -0,0 +1,61 @@
import { services } from './service-repository';
import type { ServiceUid } from './types';
const LOG_PREFIX = '[lightweight-sdk:service-registry]';
/**
* Property decorator that injects the instance registered under `uid`
* (via {@link RegisterService}) into the decorated class member. No generic
* type parameter is needed here: a legacy property decorator cannot change
* the type of the field it decorates, so the field's own type annotation
* (`private readonly logger!: LoggerService;`) is what types the member —
* annotate it as usual.
*
* Resolution is lazy and memoized: the service is looked up in
* {@link services} the first time the property is read, not at decoration
* time — this avoids depending on module evaluation order between a service
* and its consumers. After the first read, the property is replaced by the
* resolved value on the instance, so later accesses skip the repository
* lookup entirely.
* @param uid the unique identifier passed to {@link RegisterService} for the target service
* @returns a property decorator
* @example
* ```ts
* class ReportGenerator {
* ⁣@Service(LOGGER_UID)
* private readonly logger!: LoggerService;
*
* public run(): void {
* this.logger.log('generating report');
* }
* }
* ```
*/
export function Service(
uid: ServiceUid,
): (prototype: object, propertyKey: string | symbol) => void {
return (prototype: object, propertyKey: string | symbol): void => {
Object.defineProperty(prototype, propertyKey, {
configurable: true,
enumerable: true,
get(this: object): unknown {
let resolved: unknown;
try {
resolved = services.get(uid);
} catch (error) {
console.error(
`${LOG_PREFIX} Failed to inject service "${uid}" into member "${String(propertyKey)}".`,
error,
);
throw error;
}
Object.defineProperty(this, propertyKey, {
value: resolved,
enumerable: true,
configurable: true,
});
return resolved;
},
});
};
}

View file

@ -0,0 +1,14 @@
/**
* A concrete class that {@link ServiceRepository} can instantiate on its own,
* i.e. one whose constructor takes no arguments. Services registered via
* {@link RegisterService} must satisfy this shape.
*/
export interface ServiceConstructor<T = unknown> {
new (): T;
}
/**
* Unique identifier assigned to a service when it is registered in the
* {@link ServiceRepository}.
*/
export type ServiceUid = string;

View file

@ -0,0 +1,8 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}

View file

@ -211,6 +211,18 @@ importers:
packages/client/web-component: {} packages/client/web-component: {}
packages/common/service-registry:
devDependencies:
'@types/node':
specifier: 'catalog:'
version: 22.20.3
typescript:
specifier: 'catalog:'
version: 5.9.3
vitest:
specifier: 'catalog:'
version: 2.1.9(@types/node@22.20.3)
packages: packages:
'@babel/runtime@7.29.7': '@babel/runtime@7.29.7':

View file

@ -53,6 +53,15 @@ const packageJson = {
entryPoints: ['src/index.ts'], entryPoints: ['src/index.ts'],
readme: 'README.md', readme: 'README.md',
}, },
// Déclarées explicitement (même si déjà présentes à la racine, où elles
// seraient hoistées) : un package workspace sans aucune dépendance propre
// peut se retrouver sans entrée dans pnpm-lock.yaml, ce qui casse
// `pnpm install --frozen-lockfile` en CI (voir spec/ci-cd.md).
devDependencies: {
'@types/node': 'catalog:',
typescript: 'catalog:',
vitest: 'catalog:',
},
}; };
writeFileSync(join(packageDir, 'package.json'), `${JSON.stringify(packageJson, null, '\t')}\n`); writeFileSync(join(packageDir, 'package.json'), `${JSON.stringify(packageJson, null, '\t')}\n`);
@ -123,7 +132,9 @@ writeFileSync(packagesSpecPath, packagesSpec.trimEnd() + '\n' + newRow);
console.log(`Package ${npmName} créé dans packages/${category}/${name}/.`); console.log(`Package ${npmName} créé dans packages/${category}/${name}/.`);
try { try {
execFileSync('pnpm', ['install'], { cwd: ROOT, stdio: 'inherit' }); // shell: true est nécessaire sous Windows, où `pnpm` est un shim .cmd/.ps1
// que execFileSync ne résout pas via PATHEXT sans passer par un shell.
execFileSync('pnpm', ['install'], { cwd: ROOT, stdio: 'inherit', shell: true });
execFileSync( execFileSync(
'pnpm', 'pnpm',
[ [
@ -135,7 +146,7 @@ try {
'tsconfig.json', 'tsconfig.json',
'spec/packages.md', 'spec/packages.md',
], ],
{ cwd: ROOT, stdio: 'inherit' }, { cwd: ROOT, stdio: 'inherit', shell: true },
); );
} catch (error) { } catch (error) {
console.error('Étape automatique échouée, à relancer toi-même :'); console.error('Étape automatique échouée, à relancer toi-même :');

View file

@ -30,6 +30,10 @@ Un nouveau package se crée avec `pnpm create-package <client|common|server> <no
| Versioning/publish | [Changesets](https://github.com/changesets/changesets) | Gère des versions indépendantes par package + changelogs, comprend nativement `workspace:*` | | Versioning/publish | [Changesets](https://github.com/changesets/changesets) | Gère des versions indépendantes par package + changelogs, comprend nativement `workspace:*` |
| Node cible | 24 (Active LTS) en CI, `engines: ">=22"` | Node 20 est end-of-life depuis avril 2026 | | Node cible | 24 (Active LTS) en CI, `engines: ">=22"` | Node 20 est end-of-life depuis avril 2026 |
## Décorateurs et `useDefineForClassFields`
`tsconfig.base.json` force `"useDefineForClassFields": false`, bien que la cible soit `ES2022` (où ce flag vaut `true` par défaut). Sans ça, un champ de classe déclaré sans initialiseur (`logger!: LoggerService;`) est réinitialisé à `undefined` **sur l'instance** au moment de la construction, ce qui masque silencieusement tout accesseur posé sur le prototype par un décorateur de propriété (`@Service` de `@lightweight-sdk/service-registry`, `@LinkToDom` de `web-component`). C'est une interaction connue entre `useDefineForClassFields` et les décorateurs legacy (`experimentalDecorators`) — désactiver ce flag restaure la sémantique classique où un champ sans initialiseur n'émet aucun code, laissant l'accesseur du prototype pleinement fonctionner.
## Dépendances entre packages ## Dépendances entre packages
Les dépendances internes entre packages du monorepo utilisent le protocole `workspace:*` (résolu localement par pnpm, jamais via le registre distant). Le registre npm privé n'est utilisé que pour la publication et pour la consommation depuis des projets *externes* à ce monorepo. Les dépendances internes entre packages du monorepo utilisent le protocole `workspace:*` (résolu localement par pnpm, jamais via le registre distant). Le registre npm privé n'est utilisé que pour la publication et pour la consommation depuis des projets *externes* à ce monorepo.

View file

@ -35,6 +35,10 @@ Pas de bot automatique type `changesets/action` (spécifique GitHub, incertain s
1. Forgejo → Settings → Applications → générer un token avec les scopes `write:package` + `read:package`. 1. Forgejo → Settings → Applications → générer un token avec les scopes `write:package` + `read:package`.
2. L'ajouter comme secret de repo nommé `REGISTRY_TOKEN` sur **ce** repo (`lightweight-sdk`) — les secrets de repo ne sont pas partagés entre repos sur cette instance ; celui de `watchparty` ne peut pas être réutilisé tel quel. 2. L'ajouter comme secret de repo nommé `REGISTRY_TOKEN` sur **ce** repo (`lightweight-sdk`) — les secrets de repo ne sont pas partagés entre repos sur cette instance ; celui de `watchparty` ne peut pas être réutilisé tel quel.
### Troubleshooting : `pnpm-lock.yaml` sans entrée pour un nouveau package
Si `pnpm install --frozen-lockfile` échoue en CI avec `pnpm-lock.yaml has no importers["packages/<catégorie>/<nom>"] entry` alors que `pnpm install` (sans `--frozen-lockfile`) fonctionne en local sans se plaindre : un package workspace **sans aucune `dependencies`/`devDependencies` propre** peut ne pas obtenir d'entrée dans le lockfile généré par pnpm 12.4.2, même après suppression complète du lockfile et réinstallation. Constaté en ajoutant `@lightweight-sdk/service-registry` (package.json sans champ de dépendances du tout). Remède : déclarer explicitement quelques `devDependencies` (ex. `typescript`, `vitest` via `catalog:`) même si elles seraient de toute façon hoistées depuis la racine — `create-package.mjs` le fait automatiquement pour tout nouveau package. Vérifier avant de pousser : `pnpm install --frozen-lockfile` en local doit passer, pas seulement `pnpm install`.
### Troubleshooting : 404 sur un package scopé ### Troubleshooting : 404 sur un package scopé
Si `pnpm changeset:publish` échoue avec une 404 sur un nom de package scopé (`@lightweight-sdk/...`), la cause probable est le reverse proxy devant l'instance : npm encode le `/` d'un nom scopé en `%2F`, ce qu'Apache refuse par défaut. Vérifier `AllowEncodedSlashes NoDecode` dans le VirtualHost et `nocanon` sur la directive `ProxyPass`. Ce point n'a pas pu être vérifié depuis l'agent (config réseau propre à l'instance). Si `pnpm changeset:publish` échoue avec une 404 sur un nom de package scopé (`@lightweight-sdk/...`), la cause probable est le reverse proxy devant l'instance : npm encode le `/` d'un nom scopé en `%2F`, ce qu'Apache refuse par défaut. Vérifier `AllowEncodedSlashes NoDecode` dans le VirtualHost et `nocanon` sur la directive `ProxyPass`. Ce point n'a pas pu être vérifié depuis l'agent (config réseau propre à l'instance).

View file

@ -30,3 +30,4 @@ Le code source de ce package a été migré strictement à l'identique depuis l'
- Le style de formatage d'origine (4 espaces, guillemets doubles) diffère de la config Biome du monorepo (tabulations, guillemets simples) — le formatter Biome est désactivé pour ce package via le même `overrides`. - Le style de formatage d'origine (4 espaces, guillemets doubles) diffère de la config Biome du monorepo (tabulations, guillemets simples) — le formatter Biome est désactivé pour ce package via le même `overrides`.
- `REQUEST_DOM_LINK_KEY` semble ne plus être utilisé nulle part dans le code (le mécanisme actif est le tableau `__linkedProperties`) — potentiellement mort, mais pas supprimé. - `REQUEST_DOM_LINK_KEY` semble ne plus être utilisé nulle part dans le code (le mécanisme actif est le tableau `__linkedProperties`) — potentiellement mort, mais pas supprimé.
- Quelques typos mineures ("atatched", "attriute") et un commentaire en français dans `LinkToDom` qui ne correspond plus exactement au comportement (`throw` au lieu d'un simple avertissement). - Quelques typos mineures ("atatched", "attriute") et un commentaire en français dans `LinkToDom` qui ne correspond plus exactement au comportement (`throw` au lieu d'un simple avertissement).
| `@lightweight-sdk/service-registry` | common | 0.0.1 | brouillon |

View file

@ -13,6 +13,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"experimentalDecorators": true "experimentalDecorators": true,
"useDefineForClassFields": false
} }
} }

View file

@ -1,4 +1,11 @@
{ {
"files": [], "files": [],
"references": [{ "path": "./packages/client/web-component" }] "references": [
{
"path": "./packages/client/web-component"
},
{
"path": "./packages/common/service-registry"
}
]
} }