@@ -14,7 +14,9 @@ import { promisify } from 'node:util';
1414// vierge en répertoire temporaire, chargement réel du point d'entrée (le bundle UMD
1515// s'exporte en CommonJS sous Node, sans DOM), cohérence de version, puis présence des
1616// fichiers recommandés par le README dans l'arbre installé. Une release cassée est ainsi
17- // détectée avant le tag, plus jamais après.
17+ // détectée avant le tag, plus jamais après. La fumée ESM (D-025) s'y ajoute : l'entrée
18+ // import de la carte exports et un composant réel se chargent dans un processus Node
19+ // vierge, require reste vert, et le passe-plat ./dist/* résout un chemin profond.
1820
1921const execFileAsync = promisify ( execFile ) ;
2022const PROJECT_ROOT = fileURLToPath ( new URL ( '../..' , import . meta. url ) ) ;
@@ -33,6 +35,11 @@ const EXPECTED_RUNTIME_SURFACES = {
3335// EXPECTED_INTER_FONT_FILES dans build/fork/check-assets.js (les empreintes exactes sont
3436// vérifiées là-bas ; ici, seule la présence dans l'arbre installé est en jeu). Le point
3537// d'entrée des types s'y ajoute, lu dans le champ types du paquet installé.
38+ // Fumée ESM (D-025) : un composant réel du paquet (entrée ./components/* de la carte
39+ // exports) et un chemin profond représentatif servi par le passe-plat ./dist/*.
40+ const ESM_SMOKE_COMPONENT = 'filter' ;
41+ const DEEP_PATH_SPECIMEN = 'dist/css/drake.css' ;
42+
3643const README_RECOMMENDED_FILES = [
3744 'dist/css/drake.css' ,
3845 'dist/css/drake.min.css' ,
@@ -57,14 +64,18 @@ try {
5764
5865 const installed = await readInstalledManifest ( installedRoot , manifest ) ;
5966 smokeTestRuntime ( consumer , manifest ) ;
67+ await smokeTestEsmModules ( consumer , manifest ) ;
68+ await checkDeepPathResolution ( consumer , manifest ) ;
6069 const checkedFileCount = await checkRecommendedFiles ( installedRoot , installed ) ;
6170
6271 console . log (
6372 `Gate consommateur (D-024) : canal D-020 prouvé de bout en bout — ` +
6473 `${ manifest . name } @${ manifest . version } empaqueté, installé dans un consommateur ` +
6574 `vierge et chargé sans window ` +
6675 `(surfaces ${ Object . keys ( EXPECTED_RUNTIME_SURFACES ) . join ( ', ' ) } ) ; ` +
67- `${ checkedFileCount } fichiers recommandés présents.` ,
76+ `fumée ESM D-025 verte (import de l'entrée et du composant ` +
77+ `${ ESM_SMOKE_COMPONENT } , require inchangé, passe-plat ${ DEEP_PATH_SPECIMEN } ` +
78+ `résolu) ; ${ checkedFileCount } fichiers recommandés présents.` ,
6879 ) ;
6980} catch ( error ) {
7081 console . error ( `Gate consommateur en échec : ${ error . message } ` ) ;
@@ -197,6 +208,68 @@ function smokeTestRuntime(consumer, manifest) {
197208 }
198209}
199210
211+ async function smokeTestEsmModules ( consumer , manifest ) {
212+ // Fumée ESM (D-025) : l'entrée import de la carte exports (dist/esm/drake.js) et un
213+ // composant réel (dist/js/components/*.esm.js) doivent se charger depuis le
214+ // consommateur vierge, dans un processus Node séparé et sans DOM — l'amorçage se
215+ // retire de lui-même hors navigateur, et l'enregistrement du composant est gardé par
216+ // typeof window. Les surfaces attendues sont les mêmes que pour la fumée require.
217+ const script = join ( consumer , 'smoke-esm.mjs' ) ;
218+ const componentSpecifier = `${ manifest . name } /components/${ ESM_SMOKE_COMPONENT } ` ;
219+ await writeFile (
220+ script ,
221+ [
222+ `import assert from 'node:assert/strict';` ,
223+ `import Drake from ${ JSON . stringify ( manifest . name ) } ;` ,
224+ `import Component from ${ JSON . stringify ( componentSpecifier ) } ;` ,
225+ `assert.equal(typeof globalThis.window, 'undefined', 'fumée ESM sans DOM invalide : un global window est présent');` ,
226+ `assert.ok(Drake !== null && (typeof Drake === 'object' || typeof Drake === 'function'), \`l'entrée ESM n'exporte pas l'objet runtime : reçu \${typeof Drake}\`);` ,
227+ ...Object . entries ( EXPECTED_RUNTIME_SURFACES ) . map (
228+ ( [ surface , expectedType ] ) =>
229+ `assert.ok(typeof Drake[${ JSON . stringify ( surface ) } ] === ${ JSON . stringify ( expectedType ) } && Drake[${ JSON . stringify ( surface ) } ] !== null, \`surface runtime absente ou invalide sur l'export ESM : ${ surface } (attendu ${ expectedType } , reçu \${typeof Drake[${ JSON . stringify ( surface ) } ]})\`);` ,
230+ ) ,
231+ `assert.equal(Drake.version, ${ JSON . stringify ( manifest . version ) } , \`version runtime ESM incohérente : \${Drake.version} au lieu de ${ manifest . version } \`);` ,
232+ `assert.ok(Component !== null && (typeof Component === 'object' || typeof Component === 'function'), \`le composant ${ ESM_SMOKE_COMPONENT } n'exporte pas de définition : reçu \${typeof Component}\`);` ,
233+ `` ,
234+ ] . join ( '\n' ) ,
235+ ) ;
236+
237+ try {
238+ await execFileAsync ( process . execPath , [ script ] , {
239+ cwd : consumer ,
240+ encoding : 'utf8' ,
241+ maxBuffer : 16 * 1024 * 1024 ,
242+ } ) ;
243+ } catch ( error ) {
244+ const detail =
245+ error . stderr ?. trim ( ) . split ( '\n' ) . slice ( - 5 ) . join ( ' | ' ) || error . message || error ;
246+ throw new Error (
247+ `La fumée ESM a échoué (import('${ manifest . name } ') et ` +
248+ `import('${ componentSpecifier } ') depuis le consommateur) : ${ detail } ` ,
249+ { cause : error } ,
250+ ) ;
251+ }
252+ }
253+
254+ async function checkDeepPathResolution ( consumer , manifest ) {
255+ // Passe-plat ./dist/* (D-025) : la carte exports ne doit casser aucun chemin profond
256+ // existant — require.resolve d'une feuille CSS recommandée le prouve depuis le
257+ // consommateur (une carte sans passe-plat ferait échouer cette résolution).
258+ const requireFromConsumer = createRequire ( join ( consumer , 'smoke.cjs' ) ) ;
259+ const specifier = `${ manifest . name } /${ DEEP_PATH_SPECIMEN } ` ;
260+ let resolved ;
261+ try {
262+ resolved = requireFromConsumer . resolve ( specifier ) ;
263+ } catch ( error ) {
264+ throw new Error (
265+ `require.resolve('${ specifier } ') a échoué depuis le consommateur : le ` +
266+ `passe-plat ./dist/* de la carte exports est absent ou cassé (${ error . message } ).` ,
267+ { cause : error } ,
268+ ) ;
269+ }
270+ await assertNonEmptyFile ( resolved , `le chemin profond résolu ${ specifier } ` ) ;
271+ }
272+
200273async function checkRecommendedFiles ( installedRoot , installed ) {
201274 const files = [ ...README_RECOMMENDED_FILES , installed . types ] ;
202275 for ( const file of files ) {
0 commit comments