Preparare TinyDI per npm: package.json, exports map e tree-shaking
Scrivere la libreria core è un problema. Distribuirla come pacchetto che altri progetti possano installare con npm install e importare sia da ESM sia da CJS, con i tipi corretti su entrambi i lati, è un problema diverso. Questo post parla della meccanica reale: il cambio di bundler, la mappa exports, e un bug che veniva dal tooling, non da noi.
Da tsc puro a tsup
La build è passata da un'emissione diretta per-file con tsc a tsup, producendo un vero pacchetto dual ESM+CJS: dist/index.js (ESM), dist/index.cjs (CJS), e i corrispondenti file di dichiarazione dist/index.d.ts/dist/index.d.cts. Un'emissione tsc diretta produce un solo formato di modulo; un vero pacchetto dual richiede che il build tool impacchetti ed emetta entrambi, con dichiarazioni di tipo separate, dato che la risoluzione dei tipi in ESM e CJS non è esattamente la stessa cosa.
La exports map: due condizioni, ciascuna con i propri tipi
{
"exports": {
".": {
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
}
}
}
Ogni condizione — import per i consumatori ESM, require per quelli CJS — ha una propria voce types, non condivisa. Sbagliare questo (ad esempio un unico .d.ts condiviso per entrambi) può far sì che un consumatore CJS finisca con errori di tipo derivati dalla risoluzione dei tipi specifica di ESM, o viceversa, in un modo che emerge solo nella sua build, non nella nostra.
Zero dipendenze, una peer dependency
package.json non ha affatto un campo dependencies — solo devDependencies (tooling) e una peerDependencies.typescript: ">=6.0.0" opzionale. Non è un dettaglio marginale: è l'intero punto della libreria. Chiunque verifichi cosa si porta dietro aggiungendo TinyDI a un progetto ottiene una risposta reale: niente.
Un bug che era del tooling, non nostro
Il passaggio di bundling delle dichiarazioni di tsup sintetizza internamente un'opzione baseUrl, su cui TypeScript 6.0 lancia un errore bloccante, TS5101: Option 'baseUrl' is deprecated. Questo non ha nulla a che fare con il tsconfig.json di questo progetto — è un'interazione tra gli interni di tsup/rollup-plugin-dts e la politica di deprecazione più severa di TypeScript 6. La correzione è una riga sola, aggiunta specificamente a tsconfig.build.json (non al tsconfig.json base usato per editor/lint/type-check):
{
"compilerOptions": {
"ignoreDeprecations": "6.0"
}
}
Perché questo conta oltre la correzione di una riga
Riconoscere che un errore proviene dagli interni del build tool piuttosto che dalla propria configurazione cambia il modo in cui lo risolvi: l'alternativa sarebbe stata inseguire un'inesistente impostazione baseUrl nei nostri stessi file tsconfig.json, dove non è mai stato il problema reale.
Il risultato concreto
Il risultato è un pacchetto che fa tree-shaking in modo pulito (output ESM, nessun codice a livello di modulo con side-effect) e funziona allo stesso modo indipendentemente dal fatto che il bundler di chi consuma scelga la condizione import o require — verificato in pratica dai sette esempi di questo progetto, che coprono sia una toolchain frontend basata su Vite sia semplici script Node tsx, tutti in grado di risolvere correttamente lo stesso pacchetto.
Sistemare il pacchetto in sé si è rivelato necessario ma non sufficiente. Pubblicarlo per davvero — il punto in cui un package.json incontra l'infrastruttura reale di npm — ha fatto emergere una serie di problemi separati che nulla di tutto questo aveva previsto, raccontati nel post sulla release.