pnpm --filter web dev ne démarre pas sous Windows : ${PORT_WEB:-4000} n'est pas substitué par cmd.exe #22

Closed
opened 2026-09-10 09:34:35 +00:00 by nicolas-fryder · 2 comments

Les scripts dev et start d'apps/web/package.json utilisent une substitution de variable POSIX. pnpm passe les scripts par cmd.exe sous Windows, qui ne la connaît pas : la chaîne arrive telle quelle à next, qui refuse.

Reproduction

Un clone neuf du gabarit, sur Windows, pnpm install, puis :

pnpm --filter web dev

Sortie :

$ next dev --port ${PORT_WEB:-4000}
error: option '-p, --port <port>' argument '${PORT_WEB:-4000}' is invalid. '${PORT_WEB:-4000}' is not a non-negative number.
[ELIFECYCLE] Command failed with exit code 1.

start porte exactement la même chaîne, donc le même sort.

Le fichier fautif, sur main (8e70251, aujourd'hui 09:20) :

"dev": "next dev --port ${PORT_WEB:-4000}",
"start": "next start --port ${PORT_WEB:-4000}",

Ce que j'attendais, et pourquoi

Que pnpm --filter web dev démarre le front, comme pnpm --filter api dev démarre l'API. Le README du gabarit et AGENTS.md présentent les deux commandes sur le même plan, sans réserve de plateforme.

C'est d'autant plus surprenant que le socle connaît déjà ce comportement : le corps du ticket #16 (pnpm dev recompile enfin ce qu'il sert) écarte explicitement & en écrivant « pnpm passe les scripts par cmd sous Windows, où & enchaîne au lieu de mettre en fond », et choisit concurrently pour cette raison. Le même raisonnement n'a pas été appliqué à ${...} dans apps/web.

Versions

  • eqs --version : 0.8.1
  • packageManager : pnpm@11.8.0
  • Node : v24.15.0
  • Windows 11 Pro 26200

Ce que ça coûte

Bloquant pour lancer le front en développement sur un poste Windows — et toute l'équipe est sous Windows. Contourné localement en lançant pnpm exec next dev --port 4000 depuis apps/web/, sans modifier le gabarit : il n'y a donc aucun contournement à retirer dans le satellite une fois le défaut corrigé.

Non détecté par la porte : pnpm check ne joue ni dev ni start, et la CI est sous Linux, où la substitution fonctionne. C'est un défaut qui ne peut apparaître que sur le poste de quelqu'un.

Ce que je proposerais

Le plus simple, et cohérent avec ce que fait déjà apps/api : lire le port depuis l'environnement dans du JavaScript plutôt que dans une chaîne de shell. next accepte PORT directement, donc "dev": "next dev" suffirait si le port se règle par PORT — au prix de perdre le nom PORT_WEB, qui a peut-être sa raison d'être face à PORT_API.

Sinon, cross-env ou un petit script node scripts/dev.mjs qui résout process.env.PORT_WEB ?? 4000 et appelle next. C'est plus verbeux, mais c'est la seule forme qui se comporte pareil sur les deux plateformes.

Une troisième voie serait script-shell dans le .npmrc du gabarit pour forcer un shell POSIX — je la mentionne pour être complet, mais elle m'a l'air pire : elle impose bash à tous les scripts de tous les satellites pour régler deux chaînes.

Trouvé en développant le premier satellite réel (Pilotage), en vérifiant à l'écran une page qui consomme deux nouvelles opérations.

Les scripts `dev` et `start` d'`apps/web/package.json` utilisent une substitution de variable POSIX. pnpm passe les scripts par `cmd.exe` sous Windows, qui ne la connaît pas : la chaîne arrive telle quelle à `next`, qui refuse. ## Reproduction Un clone neuf du gabarit, sur Windows, `pnpm install`, puis : ``` pnpm --filter web dev ``` Sortie : ``` $ next dev --port ${PORT_WEB:-4000} error: option '-p, --port <port>' argument '${PORT_WEB:-4000}' is invalid. '${PORT_WEB:-4000}' is not a non-negative number. [ELIFECYCLE] Command failed with exit code 1. ``` `start` porte exactement la même chaîne, donc le même sort. Le fichier fautif, sur `main` (`8e70251`, aujourd'hui 09:20) : ```json "dev": "next dev --port ${PORT_WEB:-4000}", "start": "next start --port ${PORT_WEB:-4000}", ``` ## Ce que j'attendais, et pourquoi Que `pnpm --filter web dev` démarre le front, comme `pnpm --filter api dev` démarre l'API. Le `README` du gabarit et `AGENTS.md` présentent les deux commandes sur le même plan, sans réserve de plateforme. C'est d'autant plus surprenant que le socle **connaît déjà** ce comportement : le corps du ticket #16 (`pnpm dev` recompile enfin ce qu'il sert) écarte explicitement `&` en écrivant « pnpm passe les scripts par `cmd` sous Windows, où `&` enchaîne au lieu de mettre en fond », et choisit `concurrently` pour cette raison. Le même raisonnement n'a pas été appliqué à `${...}` dans `apps/web`. ## Versions - `eqs --version` : `0.8.1` - `packageManager` : `pnpm@11.8.0` - Node : `v24.15.0` - Windows 11 Pro 26200 ## Ce que ça coûte Bloquant pour lancer le front en développement sur un poste Windows — et toute l'équipe est sous Windows. Contourné localement en lançant `pnpm exec next dev --port 4000` depuis `apps/web/`, sans modifier le gabarit : il n'y a donc **aucun contournement à retirer** dans le satellite une fois le défaut corrigé. Non détecté par la porte : `pnpm check` ne joue ni `dev` ni `start`, et la CI est sous Linux, où la substitution fonctionne. C'est un défaut qui ne peut apparaître que sur le poste de quelqu'un. ## Ce que je proposerais Le plus simple, et cohérent avec ce que fait déjà `apps/api` : lire le port depuis l'environnement dans du JavaScript plutôt que dans une chaîne de shell. `next` accepte `PORT` directement, donc `"dev": "next dev"` suffirait si le port se règle par `PORT` — au prix de perdre le nom `PORT_WEB`, qui a peut-être sa raison d'être face à `PORT_API`. Sinon, `cross-env` ou un petit script `node scripts/dev.mjs` qui résout `process.env.PORT_WEB ?? 4000` et appelle `next`. C'est plus verbeux, mais c'est la seule forme qui se comporte pareil sur les deux plateformes. Une troisième voie serait `script-shell` dans le `.npmrc` du gabarit pour forcer un shell POSIX — je la mentionne pour être complet, mais elle m'a l'air pire : elle impose bash à tous les scripts de tous les satellites pour régler deux chaînes. Trouvé en développant le premier satellite réel (Pilotage), en vérifiant à l'écran une page qui consomme deux nouvelles opérations.
Author
Owner

Vrai, reproduit, corrigé. Et votre rapprochement avec #16 était le bon fil : c'est bien la même cause, non portée.

Reproduction

Mesuré hors du gabarit, sur un manifeste jetable, pour isoler pnpm de Next :

$ node dire.mjs --port ${PORT_WEB:-4000}
["--port","${PORT_WEB:-4000}"]

pnpm 11.8, Windows 11. La chaîne arrive littéralement. Rien à ajouter à votre diagnostic.

Ce que la vérification a trouvé en plus

PORT_WEB n'a jamais réglé le port, sur aucune plateforme — Linux compris.

${PORT_WEB} lit l'environnement du shell. Le .env du dépôt, lui, est chargé par apps/web/next.config.mjs, c'est-à-dire dans le processus Next, bien après que --port a été analysé. Personne n'exporte PORT_WEB dans son shell, donc sous Linux la substitution rendait toujours le défaut 4000. La ligne PORT_WEB=4000 de .env.example décrivait un réglage qui n'existait pas — exactement ce que l'en-tête de ce fichier appelle « un mensonge qui se propage par copier-coller ».

Le correctif fait donc les deux : il rend le script portable, et il fait marcher la variable.

La correction

Votre deuxième piste, apps/web/scripts/next.mjs. Le .env racine est chargé d'abord, avec le même primitif que next.config.mjs (process.loadEnvFile, natif, qui ne remplace pas une variable déjà posée — la précédence reste la bonne), puis le port est résolu, puis next est lancé.

Un détail qui compte : le script lance le point d'entrée JavaScript de next avec le Node courant, pas le shim .cmd. Passer par un shim aurait imposé shell: true, c'est-à-dire ramener cmd.exe — la cause du défaut.

Vérifié trois fois :

port obtenu d'où
4000 défaut, aucune variable
4321 PORT_WEB dans l'environnement du shell
4567 PORT_WEB dans le .env racine

J'ai écarté script-shell dans le .npmrc pour la raison que vous donnez, et cross-env parce qu'il pose des variables, il n'en résout pas avec un défaut.

Les quatre autres occurrences

Vous en aviez vu deux. En cherchant la classe dans toute la flotte, il y en avait six :

dépôt signalé ?
eqs-satellite-template/apps/web par vous
eqs-satellite-template/variantes/next-fullstack/apps/web non
eqs-demo/apps/web (${PORT:-4000}) non

La variante n'a pas besoin d'une copie du script : son README fusionne variantes/next-fullstack/apps/web/. sur apps/web/, donc celui de la base arrive avec. Seul son manifeste, qui écrase l'autre, devait changer.

Aucune autre construction POSIX ailleurs dans la flotte — pas de $(...), pas d'apostrophes simples, pas de /dev/null, pas de préfixe VAR=x. C'est un résultat et pas une absence de résultat : le reste est sain.

Pour que ça ne revienne pas

Nouveau contrôle bloquant tooling.script-shell-posix dans @eqs/tooling : un script de manifeste ne peut pas supposer un shell POSIX. Cinq formes refusées (${...}, $(...), $NOM, > /dev/null, apostrophes simples) et cinq formes saines qui doivent rester vertes (&&, ||, guillemets doubles, cross-env, un appel node) — les cas verts sont dans le check du package au même titre que les rouges, parce qu'une porte qui refuse un script sain se fait éteindre.

Ce qu'il ne couvre pas, et c'est dit dans le code : le préfixe VAR=x cmd, parce que cross-env FOO=bar cmd a la même forme et fonctionne.

L'ironie, parce qu'elle mérite d'être écrite

La forme correcte — un node scripts/x.mjs qui lit process.env.PORT_WEB — faisait rougir config.env-example en bloquant. Ce contrôle ne cherchait les noms de variables que dans les compose, les package.json et les Dockerfile : sortie du manifeste, la variable devenait « non référencée ». La porte punissait exactement ce qu'elle existe pour encourager, comme dans #47. Les scripts/*.{mjs,js} sont maintenant lus.

Et en élargissant ce scan, il devenait un includes() sur du JavaScript, commentaires compris — l'en-tête du script corrigé cite PORT_WEB quatre fois en prose. C'est la classe de #47 encore, dans le sens du faux négatif cette fois : une variable citée en commentaire serait passée pour utilisée, et une ligne morte de .env.example aurait survécu. Les commentaires sont écartés, éprouvé dans les deux sens.

État

Trois branches, à fusionner dans cet ordre ou non, elles sont indépendantes :

  • eqs-satellite-template → fix/scripts-windows, basée sur main (pas sur chore/socle-0-8-1, pour qu'elle vous débloque sans attendre)
  • eqs-demo → fix/scripts-windows
  • eqs-platform → chasse/knip-et-code-mort, qui porte le contrôle

Je laisse ce ticket ouvert : la correction du gabarit vous débloque dès la fusion, mais le contrôle n'existera chez vous qu'à la prochaine version du socle. À vous de constater.

Un point à votre main : votre contournement pnpm exec next dev --port 4000 reste correct après la correction, mais il ignore PORT_WEB, qui fonctionne maintenant.

Vrai, reproduit, corrigé. Et votre rapprochement avec #16 était le bon fil : c'est bien la même cause, non portée. ## Reproduction Mesuré hors du gabarit, sur un manifeste jetable, pour isoler pnpm de Next : ``` $ node dire.mjs --port ${PORT_WEB:-4000} ["--port","${PORT_WEB:-4000}"] ``` pnpm 11.8, Windows 11. La chaîne arrive littéralement. Rien à ajouter à votre diagnostic. ## Ce que la vérification a trouvé en plus `PORT_WEB` n'a **jamais** réglé le port, sur aucune plateforme — Linux compris. `${PORT_WEB}` lit l'environnement du *shell*. Le `.env` du dépôt, lui, est chargé par `apps/web/next.config.mjs`, c'est-à-dire **dans le processus Next**, bien après que `--port` a été analysé. Personne n'exporte `PORT_WEB` dans son shell, donc sous Linux la substitution rendait toujours le défaut `4000`. La ligne `PORT_WEB=4000` de `.env.example` décrivait un réglage qui n'existait pas — exactement ce que l'en-tête de ce fichier appelle « un mensonge qui se propage par copier-coller ». Le correctif fait donc les deux : il rend le script portable, et il fait marcher la variable. ## La correction Votre deuxième piste, `apps/web/scripts/next.mjs`. Le `.env` racine est chargé d'abord, avec le même primitif que `next.config.mjs` (`process.loadEnvFile`, natif, qui ne remplace pas une variable déjà posée — la précédence reste la bonne), puis le port est résolu, puis `next` est lancé. Un détail qui compte : le script lance le point d'entrée **JavaScript** de `next` avec le Node courant, pas le shim `.cmd`. Passer par un shim aurait imposé `shell: true`, c'est-à-dire ramener `cmd.exe` — la cause du défaut. Vérifié trois fois : | port obtenu | d'où | | --- | --- | | 4000 | défaut, aucune variable | | 4321 | `PORT_WEB` dans l'environnement du shell | | 4567 | `PORT_WEB` dans le `.env` racine | J'ai écarté `script-shell` dans le `.npmrc` pour la raison que vous donnez, et `cross-env` parce qu'il pose des variables, il n'en résout pas avec un défaut. ## Les quatre autres occurrences Vous en aviez vu deux. En cherchant la classe dans toute la flotte, il y en avait six : | dépôt | signalé ? | | --- | --- | | `eqs-satellite-template/apps/web` | par vous | | `eqs-satellite-template/variantes/next-fullstack/apps/web` | non | | `eqs-demo/apps/web` (`${PORT:-4000}`) | non | La variante n'a pas besoin d'une copie du script : son `README` fusionne `variantes/next-fullstack/apps/web/.` sur `apps/web/`, donc celui de la base arrive avec. Seul son manifeste, qui écrase l'autre, devait changer. Aucune autre construction POSIX ailleurs dans la flotte — pas de `$(...)`, pas d'apostrophes simples, pas de `/dev/null`, pas de préfixe `VAR=x`. C'est un résultat et pas une absence de résultat : le reste est sain. ## Pour que ça ne revienne pas Nouveau contrôle **bloquant** `tooling.script-shell-posix` dans `@eqs/tooling` : un script de manifeste ne peut pas supposer un shell POSIX. Cinq formes refusées (`${...}`, `$(...)`, `$NOM`, `> /dev/null`, apostrophes simples) et cinq formes saines qui doivent rester vertes (`&&`, `||`, guillemets doubles, `cross-env`, un appel `node`) — les cas verts sont dans le `check` du package au même titre que les rouges, parce qu'une porte qui refuse un script sain se fait éteindre. Ce qu'il ne couvre pas, et c'est dit dans le code : le préfixe `VAR=x cmd`, parce que `cross-env FOO=bar cmd` a la même forme et fonctionne. ## L'ironie, parce qu'elle mérite d'être écrite La forme correcte — un `node scripts/x.mjs` qui lit `process.env.PORT_WEB` — faisait **rougir** `config.env-example` en bloquant. Ce contrôle ne cherchait les noms de variables que dans les compose, les `package.json` et les `Dockerfile` : sortie du manifeste, la variable devenait « non référencée ». La porte punissait exactement ce qu'elle existe pour encourager, comme dans #47. Les `scripts/*.{mjs,js}` sont maintenant lus. Et en élargissant ce scan, il devenait un `includes()` sur du JavaScript, commentaires compris — l'en-tête du script corrigé cite `PORT_WEB` quatre fois en prose. C'est la classe de #47 encore, dans le sens du faux négatif cette fois : une variable citée en commentaire serait passée pour utilisée, et une ligne morte de `.env.example` aurait survécu. Les commentaires sont écartés, éprouvé dans les deux sens. ## État Trois branches, à fusionner dans cet ordre ou non, elles sont indépendantes : - `eqs-satellite-template` → `fix/scripts-windows`, basée sur `main` (pas sur `chore/socle-0-8-1`, pour qu'elle vous débloque sans attendre) - `eqs-demo` → `fix/scripts-windows` - `eqs-platform` → `chasse/knip-et-code-mort`, qui porte le contrôle Je laisse ce ticket ouvert : la correction du gabarit vous débloque dès la fusion, mais le contrôle n'existera chez vous qu'à la prochaine version du socle. À vous de constater. Un point à votre main : votre contournement `pnpm exec next dev --port 4000` reste correct après la correction, mais il ignore `PORT_WEB`, qui fonctionne maintenant.
Author
Owner

Corrigé, et par la voie que vous proposiez en second : le port est résolu en JavaScript avant d'appeler Next. apps/web/package.json porte node scripts/next.mjs dev et node scripts/next.mjs start.

Le script va plus loin que la substitution qu'il remplace, et son en-tête dit pourquoi : ${PORT_WEB} ne servait à rien même sous Linux, puisqu'il lit l'environnement du shell — que rien ne remplit. Le .env du dépôt est chargé par next.config.mjs, donc dans le processus Next, bien après l'analyse de --port. PORT_WEB=4000 dans .env n'avait donc aucun effet sur aucune plateforme : la ligne de .env.example décrivait un réglage qui n'existait pas. Ici le .env est chargé avant de résoudre le port.

Deux détails qui valaient d'être trouvés : next et pas next.cmd (un shim .cmd obligerait à shell: true, c'est-à-dire à retomber sur cmd.exe — exactement la cause de ce ticket), et ...process.argv.slice(3) pour que pnpm --filter web dev -- --turbo ne perde plus ses arguments en silence.

La classe a été chassée : six occurrences dans la flotte, dont eqs-demo, où elle n'avait pas été signalée.

Fermé après vérification sur dev le 2026-09-21. Ce ticket restait ouvert faute d'un Closes #22 dans le commit, pas faute de correctif.

Corrigé, et par la voie que vous proposiez en second : le port est résolu **en JavaScript** avant d'appeler Next. `apps/web/package.json` porte `node scripts/next.mjs dev` et `node scripts/next.mjs start`. Le script va plus loin que la substitution qu'il remplace, et son en-tête dit pourquoi : `${PORT_WEB}` ne servait à rien **même sous Linux**, puisqu'il lit l'environnement du *shell* — que rien ne remplit. Le `.env` du dépôt est chargé par `next.config.mjs`, donc **dans** le processus Next, bien après l'analyse de `--port`. `PORT_WEB=4000` dans `.env` n'avait donc aucun effet sur aucune plateforme : la ligne de `.env.example` décrivait un réglage qui n'existait pas. Ici le `.env` est chargé **avant** de résoudre le port. Deux détails qui valaient d'être trouvés : `next` et pas `next.cmd` (un shim `.cmd` obligerait à `shell: true`, c'est-à-dire à retomber sur `cmd.exe` — exactement la cause de ce ticket), et `...process.argv.slice(3)` pour que `pnpm --filter web dev -- --turbo` ne perde plus ses arguments en silence. La classe a été chassée : six occurrences dans la flotte, dont `eqs-demo`, où elle n'avait pas été signalée. Fermé après vérification sur `dev` le 2026-09-21. Ce ticket restait ouvert faute d'un `Closes #22` dans le commit, pas faute de correctif.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Infrastructure/eqs-satellite-template#22
No description provided.