fix(packaging): inline @arboretum/shared dans dist/ — tarball autonome (abandon de bundleDependencies)
Some checks failed
CI / Build & test (Node 24) (push) Has been cancelled
CI / Pack & boot smoke (Node 22) (push) Has been cancelled
CI / Build & test (Node 22) (push) Has been cancelled

bundleDependencies d'un paquet *workspace* est instable selon l'environnement npm
(mode -w, exécution en root sur un runner CI) : npm voyait shared comme un lien
workspace et n'embarquait AUCUN fichier ("bundled files: 0"), publiant un tarball
cassé — `Cannot find module '@arboretum/shared'` au démarrage chez le consommateur.
C'est ce que détectait (à juste titre) le job pack-smoke.

Nouveau mécanisme, identique sur tout environnement : scripts/inline-shared.mjs
(hook prepack) copie le JS compilé de shared dans dist/_shared/ et réécrit l'import
bare '@arboretum/shared' des .js du serveur vers ce chemin relatif. shared passe en
devDependency (résolu en dev via le symlink workspace, jamais exigé du consommateur).
Plus de node_modules embarqué ni de symlink dans le tarball.

ci.yml (pack-smoke) packe désormais en -w (comme release.yml) et asserte l'autonomie
(dist/_shared présent + zéro import bare) avant le boot smoke.

Validé de bout en bout sur checkout propre : npm ci + build + pack -w + install dans
projet vierge + boot -> HTTP 401, et 263 tests verts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-19 09:46:05 +02:00
parent d342d0e573
commit 842f17fad3
5 changed files with 88 additions and 67 deletions

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env node
// Inline @arboretum/shared (paquet workspace NON publié) directement dans le dist du serveur,
// pour que le tarball npm soit 100 % autonome. Branché sur le hook "prepack".
//
// Pourquoi PAS bundleDependencies : embarquer une dépendance qui est aussi un *workspace* via
// bundleDependencies est instable selon l'environnement npm (mode -w, exécution en root sur un
// runner CI, version d'arborist) — npm voit le nœud comme un lien workspace et n'embarque parfois
// AUCUN fichier ("bundled files: 0"), produisant un paquet cassé chez le consommateur. On élimine
// donc toute magie de bundling : on copie le JS compilé de shared dans dist/_shared et on réécrit
// l'import bare '@arboretum/shared' du serveur vers ce chemin relatif. Zéro node_modules embarqué,
// zéro symlink, résultat identique partout.
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join, dirname, relative, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const serverDir = join(dirname(fileURLToPath(import.meta.url)), '..');
const serverDist = join(serverDir, 'dist');
const sharedDist = join(serverDir, '..', 'shared', 'dist');
const inlineDir = join(serverDist, '_shared');
for (const [label, p] of [['dist serveur', serverDist], ['dist shared', sharedDist]]) {
if (!existsSync(p)) {
console.error(`inline-shared: ${label} introuvable (${p}) — lance "npm run build" avant le pack.`);
process.exit(1);
}
}
// 1) Copier le JS compilé de shared dans dist/_shared (uniquement *.js : seul le runtime compte ;
// les .d.ts/.map ne sont de toute façon pas publiés via le glob `files`). index.js réexporte
// ./protocol.js et ./api.js en relatif → la copie complète préserve la résolution interne.
rmSync(inlineDir, { recursive: true, force: true });
mkdirSync(inlineDir, { recursive: true });
let copied = 0;
for (const name of readdirSync(sharedDist)) {
if (name.endsWith('.js')) {
cpSync(join(sharedDist, name), join(inlineDir, name));
copied++;
}
}
if (copied === 0) {
console.error(`inline-shared: aucun .js dans ${sharedDist} — shared n'est pas compilé.`);
process.exit(1);
}
// 2) Réécrire l'import bare '@arboretum/shared' de chaque .js du serveur vers le chemin relatif
// (POSIX) pointant sur dist/_shared/index.js, calculé par fichier (profondeur variable).
const walk = (dir) =>
readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
const p = join(dir, e.name);
if (e.isDirectory()) return p === inlineDir ? [] : walk(p); // ne pas se réécrire soi-même
return e.name.endsWith('.js') ? [p] : [];
});
let rewritten = 0;
for (const file of walk(serverDist)) {
const src = readFileSync(file, 'utf8');
if (!src.includes('@arboretum/shared')) continue;
let rel = relative(dirname(file), join(inlineDir, 'index.js')).split(sep).join('/');
if (!rel.startsWith('.')) rel = `./${rel}`;
const out = src.replaceAll(`'@arboretum/shared'`, `'${rel}'`).replaceAll(`"@arboretum/shared"`, `"${rel}"`);
if (out !== src) {
writeFileSync(file, out);
rewritten++;
}
}
if (rewritten === 0) {
console.error(`inline-shared: aucun import '@arboretum/shared' réécrit dans ${serverDist} — build manquant ?`);
process.exit(1);
}
console.log(`inline-shared: ${copied} fichier(s) shared -> dist/_shared, import réécrit dans ${rewritten} fichier(s) serveur`);

View File

@@ -1,43 +0,0 @@
#!/usr/bin/env node
// Embarque @arboretum/shared (dépendance runtime non publiée) DANS le tarball npm.
// Branché sur le hook "prepack", aux côtés de bundleDependencies dans package.json.
//
// Pourquoi un vrai dossier et pas le symlink workspace : sous npm workspaces, shared
// est seulement symlinké dans le node_modules racine ; `npm pack` n'embarque une
// bundleDependency que si elle existe comme VRAI dossier dans le node_modules du
// paquet packé (packages/server/node_modules/@arboretum/shared). On le matérialise ici.
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const serverDir = join(dirname(fileURLToPath(import.meta.url)), '..');
const sharedDir = join(serverDir, '..', 'shared');
const sharedDist = join(sharedDir, 'dist');
const target = join(serverDir, 'node_modules', '@arboretum', 'shared');
if (!existsSync(sharedDist)) {
console.error(
`vendor-shared: ${sharedDist} introuvable — lance "npm run build" (shared doit être compilé) avant le pack.`,
);
process.exit(1);
}
// package.json minimal et déterministe : on reprend les champs de résolution réels de
// shared (version incluse, pour rester synchro), sans scripts ni files inutiles au runtime.
const shared = JSON.parse(readFileSync(join(sharedDir, 'package.json'), 'utf8'));
const minimal = {
name: shared.name,
version: shared.version,
type: shared.type,
main: shared.main,
types: shared.types,
exports: shared.exports,
};
// Idempotent : on repart d'un dossier propre à chaque pack.
rmSync(target, { recursive: true, force: true });
mkdirSync(target, { recursive: true });
writeFileSync(join(target, 'package.json'), JSON.stringify(minimal, null, 2) + '\n');
cpSync(sharedDist, join(target, 'dist'), { recursive: true });
console.log(`vendor-shared: embarqué ${shared.name}@${shared.version} -> ${target}`);