chore: install via pnpm + polyfill depuis Gitea (git-https) + lien local réactif

Passe l'installation de bun à pnpm (runtime/build/test restent bun).
Dépendance prod @ng-eventually/client résolue depuis le Gitea public en
git+https (committée, reproductible via pnpm-lock.yaml). Script
link:polyfill (S2 copie-overlay + watcher) pour un lien local réactif
préservant l'instance @ng-org unique. bun (peer de bun-plugin-tailwind)
approuvé au build (pnpm.onlyBuiltDependencies) pour que
node_modules/.bin/bun soit un vrai binaire. Dockerfile: install pnpm
avec git + node dans l'image, runtime bun inchangé. Doctrine tech-stack
(deployment, stack-and-commands) mise à jour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
This commit is contained in:
Sylvain Duchesne
2026-07-14 12:52:30 +02:00
parent 62693667a8
commit f6fc3c262e
7 changed files with 6544 additions and 1393 deletions
@@ -1,17 +1,19 @@
---
type: knowledge
summary: Déploiement — Dockerfile multi-stage Bun Alpine qui lance `bun run start` depuis src/ (pas dist/), EXPOSE 3000, env PORT/NODE_ENV ; aucun CI/CD committé ; dev passe par le wrapper portless
last_checked: 2026-06-15
summary: Déploiement — Dockerfile multi-stage Bun Alpine ; install via pnpm (git+node dans l'image) mais runtime bun ; lance `bun run start` depuis src/ (pas dist/), EXPOSE 3000, env PORT/NODE_ENV ; aucun CI/CD committé ; dev passe par le wrapper portless
last_checked: 2026-07-14
---
# Déploiement & infra
## Dockerfile
Un `Dockerfile` existe (multi-stage Bun Alpine) :
- `FROM oven/bun:1-alpine`, stages `install` (`bun install --frozen-lockfile` depuis `package.json` + `bun.lock`) puis `release` (copie `node_modules` + source).
Un `Dockerfile` existe (multi-stage Bun Alpine). **L'installation passe par pnpm, mais le runtime/build/test restent bun** (cf. [[knowledge_stack-and-commands]]) :
- `FROM oven/bun:1-alpine`, stage `install` : `apk add --no-cache git nodejs npm` puis `npm install -g pnpm@10.26.0` (l'image bun n'a ni Node ni pnpm ; l'`apk nodejs` d'Alpine n'embarque pas corepack), `COPY package.json pnpm-lock.yaml`, puis `pnpm install --frozen-lockfile`. `git` est requis car `@ng-eventually/client` est une dépendance **git+https** publique (Gitea, sans auth). Stage `release` : copie `node_modules` + source.
- `ENV NODE_ENV=production`, `USER bun`, `EXPOSE 3000/tcp`, `ENTRYPOINT ["bun","run","start"]`.
**Piège `bun` peer** : `bun-plugin-tailwind` déclare `bun` en peerDependency → pnpm matérialise le paquet npm `bun` et **crée un shim `node_modules/.bin/bun`** qui shadow le `bun` du PATH sous `bun run`/`pnpm run`. Son postinstall est ignoré par défaut → shim cassé → `bun run start` échoue. Corrigé en approuvant le build : `pnpm.onlyBuiltDependencies: ["bun"]` dans `package.json` (le postinstall télécharge le vrai binaire). Sans ça, toute la migration pnpm casse le démarrage.
**Quirk** : `start` = `NODE_ENV=production bun src/index.ts` → le conteneur **exécute la source TypeScript directement** (Bun transpile à la volée), il **n'utilise pas `dist/`**. Le `bun run build` (→ `dist/`) n'est donc **pas** sur le chemin de prod par défaut. Si on veut servir le build, il faut changer l'entrypoint.
## CI/CD
@@ -26,3 +28,5 @@ Un `Dockerfile` existe (multi-stage Bun Alpine) :
## Dev
`bun run dev` = **`portless festipod bun --hot src/index.ts`** — passe par le wrapper **`portless`** (outil externe de gestion de port), pas un `bun --hot` nu. HMR actif hors production.
**Lien local réactif du polyfill** : en prod la dépendance `@ng-eventually/client` vient de Gitea (git+https, figée par `pnpm-lock.yaml`). Pour éditer le polyfill localement et voir les changements en direct, `pnpm run link:polyfill` (script `scripts/link-polyfill.ts`, stratégie S2) remplace `node_modules/@ng-eventually/client` par une **copie réelle** de la source locale (`…/ng-eventually-js/packages/client`) — **sans** son propre `node_modules/@ng-org` — et resynchronise `src/` à chaque édition. C'est ce qui garantit **une seule instance `@ng-org/web`** (un seul verifier) : un symlink vers le checkout monorepo, lui, embarque son `@ng-org` → 2ᵉ instance → SDK cassé. Revenir à l'état committé : `pnpm install`.
@@ -1,6 +1,6 @@
---
type: knowledge
summary: Composants de la stack (Bun, React, NextGraph, Storybook, Cucumber, Tailwind-dans-le-build) et liste réelle des scripts package.json, dont les quirks (cucumber via node+tsx, build:orm au chemin périmé, build:ng pour le fork local)
summary: Composants de la stack (Bun runtime/build/test, install via pnpm, React, NextGraph, Storybook, Cucumber, Tailwind-dans-le-build) et liste réelle des scripts package.json, dont les quirks (cucumber via node+tsx, build:orm au chemin périmé, build:ng pour le fork local, link:polyfill pour le lien local réactif)
---
# Stack & commandes
@@ -10,6 +10,7 @@ summary: Composants de la stack (Bun, React, NextGraph, Storybook, Cucumber, Tai
| Couche | Techno |
|---|---|
| Runtime / bundler / test | **Bun** (cf. [[rule_bun-first]]) |
| **Installation des deps** | **pnpm** (`pnpm install`, `pnpm-lock.yaml`) — **seule** l'install passe à pnpm ; runtime/build/test restent bun. Motif : `@ng-eventually/client` est résolu depuis Gitea en **git+https** (pnpm gère proprement `git+…#main&path:/packages/client` + le dédoublonnage des peers `@ng-org`). Ne pas rebasculer l'install vers bun/npm. |
| UI | **React** (mobile-first, largeur max 768px — style dans concept `app-architecture`) |
| Données | **NextGraph** P2P local-first (concept `data-layer`) |
| Build CSS | **Tailwind** (`tailwindcss` + `bun-plugin-tailwind`) — présent dans le build, mais les écrans stylent via `app-*`/inline, pas d'utilitaires Tailwind (cf. concept `app-architecture`) |
@@ -32,6 +33,7 @@ summary: Composants de la stack (Bun, React, NextGraph, Storybook, Cucumber, Tai
| `steps:extract` | `bun scripts/extract-step-definitions.ts` |
| `build:orm` | `rdf-orm build --input ./src/shapes/shex --output ./src/shapes/orm` |
| `build:ng` | `bash scripts/build-ng-packages.sh` — (re)build des paquets NextGraph depuis une source locale (outil optionnel) |
| `link:polyfill` | `bun scripts/link-polyfill.ts` — lien local **réactif** du polyfill `@ng-eventually/client` (stratégie S2 : copie-overlay + watcher), préserve l'instance `@ng-org` unique. Détails dans [[knowledge_deployment]]. |
| `storybook` / `build-storybook` | Storybook dev (6006) / build statique |
## Pièges
+12 -4
View File
@@ -1,11 +1,19 @@
# Use the official Bun image
# Use the official Bun image (runtime stays Bun; only install moves to pnpm)
FROM oven/bun:1-alpine AS base
WORKDIR /app
# Install dependencies
# Install dependencies with pnpm.
# - git: the @ng-eventually/client polyfill is a git+https (public Gitea) dependency → no auth.
# - nodejs + npm: pnpm is a Node CLI; we pin the exact pnpm version via `npm i -g`
# (Alpine's nodejs package does not bundle corepack).
# The `bun` npm peer (pulled by bun-plugin-tailwind) is approved to build in package.json
# (pnpm.onlyBuiltDependencies) so node_modules/.bin/bun is a real binary — required because
# `bun run start` puts node_modules/.bin ahead of PATH.
FROM base AS install
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
RUN apk add --no-cache git nodejs npm \
&& npm install -g pnpm@10.26.0
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Copy source code and build assets
FROM base AS release
-1383
View File
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -17,11 +17,12 @@
"build:orm": "rdf-orm build --input ./src/shapes/shex --output ./src/shapes/orm",
"validate": "bun scripts/validate.ts",
"build:ng": "bash scripts/build-ng-packages.sh",
"link:polyfill": "bun scripts/link-polyfill.ts",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"dependencies": {
"@ng-eventually/client": "/home/sylvain/projects/nextgraph/ng-eventually-js/packages/client",
"@ng-eventually/client": "git+https://gitea.reconnexion.apps.gueraud.net/Reconnexion/ng-eventually.git#main&path:/packages/client",
"@ng-org/alien-deepsignals": "0.1.2-alpha.11",
"@ng-org/orm": "0.1.2-alpha.18",
"@ng-org/shex-orm": "0.1.2-alpha.8",
@@ -58,5 +59,10 @@
"@storybook/addon-a11y": "^10.3.5",
"@storybook/addon-docs": "^10.3.5",
"@storybook/addon-onboarding": "^10.3.5"
},
"pnpm": {
"onlyBuiltDependencies": [
"bun"
]
}
}
+6408
View File
File diff suppressed because it is too large Load Diff
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env bun
/**
* link-polyfill.ts — Reactive local link for the @ng-eventually/client polyfill (S2).
*
* WHY S2 (copy-overlay) and not a symlink (S1):
* The committed prod dependency installs @ng-eventually/client from Gitea (git+https)
* into pnpm's store WITHOUT its own node_modules/@ng-org → @ng-org/web resolves up to
* Festipod → ONE @ng-org instance (one verifier). The local polyfill CHECKOUT, however,
* carries its own node_modules/@ng-org/* (symlinks into the ng-eventually-js monorepo
* store). Symlinking node_modules/@ng-eventually/client to that checkout puts the
* checkout's @ng-org in the resolution path → a SECOND @ng-org instance → broken SDK
* (two verifiers). So we overlay a real directory that contains ONLY the polyfill's
* source (no node_modules) and keep it in sync by copying — @ng-org still resolves to
* Festipod, single instance preserved.
*
* WHAT IT DOES:
* 1. Replaces node_modules/@ng-eventually/client (the pnpm store symlink) with a real
* directory holding the local polyfill's package.json + src (NO node_modules).
* 2. Asserts the single-instance invariant (same @ng-org/web realpath from Festipod and
* from the overlay) — aborts if it would break.
* 3. Watches the local polyfill src and copies each change into the overlay, so
* `bun --hot` (bun run dev) reloads the edited file live.
*
* USAGE (reactive dev):
* Terminal 1: pnpm run link:polyfill # overlays local source, then watches
* Terminal 2: bun run dev # portless festipod bun --hot src/index.ts
* Edit files under packages/client/src → they land in node_modules → bun --hot reloads.
*
* pnpm run link:polyfill --once # overlay + verify, no watch (CI / one-shot)
* Return to the committed git-installed dependency: pnpm install
*
* Override the local checkout path with NG_EVENTUALLY_LOCAL=/path/to/packages/client.
*/
import { existsSync, lstatSync, mkdirSync, rmSync, cpSync, copyFileSync, realpathSync } from "node:fs";
import { watch } from "node:fs";
import { join, dirname } from "node:path";
const FESTIPOD = realpathSync(join(import.meta.dir, ".."));
const LOCAL =
process.env.NG_EVENTUALLY_LOCAL ??
"/home/sylvain/projects/nextgraph/ng-eventually-js/packages/client";
const TARGET = join(FESTIPOD, "node_modules", "@ng-eventually", "client");
const SRC_LOCAL = join(LOCAL, "src");
const SRC_TARGET = join(TARGET, "src");
const ONCE = process.argv.includes("--once");
function fail(msg: string): never {
console.error(`✖ link:polyfill — ${msg}`);
process.exit(1);
}
if (!existsSync(join(LOCAL, "package.json"))) {
fail(`local polyfill not found at ${LOCAL} (set NG_EVENTUALLY_LOCAL to override)`);
}
// 1. Replace the pnpm store symlink with a real overlay dir (metadata + src, NO node_modules).
console.log(`→ overlaying local polyfill: ${LOCAL}`);
if (existsSync(TARGET) || lstatSync(TARGET, { throwIfNoEntry: false })) {
rmSync(TARGET, { recursive: true, force: true });
}
mkdirSync(TARGET, { recursive: true });
for (const meta of ["package.json", "tsconfig.json", "README.md"]) {
const from = join(LOCAL, meta);
if (existsSync(from)) copyFileSync(from, join(TARGET, meta));
}
// Copy src fresh (NEVER a node_modules dir — that is what guarantees single @ng-org instance).
cpSync(SRC_LOCAL, SRC_TARGET, { recursive: true });
// 2. Assert the single-instance invariant.
const fromFestipod = realpathSync(Bun.resolveSync("@ng-org/web", FESTIPOD));
const overlayReal = realpathSync(TARGET);
const fromPolyfill = realpathSync(Bun.resolveSync("@ng-org/web", overlayReal));
console.log(` @ng-org/web (Festipod): ${fromFestipod}`);
console.log(` @ng-org/web (overlay) : ${fromPolyfill}`);
if (fromFestipod !== fromPolyfill) {
fail(
"single-instance invariant BROKEN — @ng-org/web resolves to two different realpaths.\n" +
" The overlay must not contain its own node_modules/@ng-org. Aborting.",
);
}
console.log("✓ single @ng-org/web instance preserved");
if (ONCE) {
console.log("✓ overlay ready (--once, not watching)");
process.exit(0);
}
// 3. Watch and copy on change so `bun --hot` sees live edits.
console.log(`👀 watching ${SRC_LOCAL}${SRC_TARGET} (Ctrl-C to stop)`);
watch(SRC_LOCAL, { recursive: true }, (_event, filename) => {
if (!filename) return;
const from = join(SRC_LOCAL, filename);
const to = join(SRC_TARGET, filename);
try {
if (existsSync(from)) {
mkdirSync(dirname(to), { recursive: true });
copyFileSync(from, to);
console.log(`${filename}`);
} else if (existsSync(to)) {
rmSync(to, { force: true });
console.log(`${filename} (removed)`);
}
} catch (err) {
console.error(` ! failed to sync ${filename}:`, err);
}
});