Skip to content

Commit f3ca412

Browse files
Test-bot-cellclaude
andcommitted
feat: D-025 — sortie ESM, carte exports, sideEffects (véhicule 0.2.0)
Deux livraisons ESM par la chaîne Rollup existante, UMD prouvé identique à l'octet : - bundles dist/js/drake.esm.js, drake-core.esm.js, drake-util.esm.js (+ .esm.min.js) et les 13 composants .esm.js — externals via paths relatifs, aucune duplication d'instance, variante min cohérente ; - arborescence dist/esm/ (preserveModules, 112 modules, imports relatifs avec extension, manifeste local type module émis par le build : import Node natif sans re-parse). package.json : carte exports (., ./core, ./components/*, passe-plat ./dist/* obligatoire, ./package.json), main/style/types conservés, sideEffects établi par lecture du code émis (3 modules à effets top-level + installateurs ; utilitaires et mixins élagables). Gates étendus : fumée ESM au gate consommateur (import entrée + composant, require inchangé, passe-plat résolu), 6 budgets de taille ajoutés (21 artefacts sous budget), test de consommation des types PAR la carte (traceResolution prouvé), manifeste 45 -> 189 fichiers, G9 couvre dist/esm/. Matrice C0-C3 verte sans recapture. README : usage ESM documenté. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent b06f0fb commit f3ca412

157 files changed

Lines changed: 48876 additions & 30 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
/dist/*
22
!/dist/fork-manifest.json
33
!/dist/css/
4+
!/dist/esm/
45
!/dist/js/
56
/dist/js/*
67
!/dist/js/components/
78
!/dist/js/drake.js
89
!/dist/js/drake.min.js
10+
!/dist/js/drake.esm.js
11+
!/dist/js/drake.esm.min.js
912
!/dist/js/drake-core.js
1013
!/dist/js/drake-core.min.js
14+
!/dist/js/drake-core.esm.js
15+
!/dist/js/drake-core.esm.min.js
16+
!/dist/js/drake-util.esm.js
17+
!/dist/js/drake-util.esm.min.js
1118
!/dist/fonts/
1219
!/dist/types/
1320
/node_modules

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ dans [`docs/fork/CHANGELOG-uikit-amont.md`](docs/fork/CHANGELOG-uikit-amont.md).
1717
release outillé `pnpm release` (préflight, tag signé, release GitHub, vérification
1818
HTTP des chemins CDN), budgets de taille épinglés des artefacts principaux dans
1919
`pnpm verify` (relèvement = décision consignée, jamais silencieux).
20+
- Sortie ESM (décision D-025, amendant D-017) : bundles `drake.esm.js`,
21+
`drake-core.esm.js` et composants `.esm.js`, arborescence de modules préservés
22+
`dist/esm/` (tree-shaking réel), carte `exports` avec passe-plat `./dist/*`,
23+
`sideEffects` déclaré, fumée ESM au gate consommateur, budgets et test de types
24+
étendus — l'UMD reste livré à l'identique.
2025

2126
## 0.1.1 — 2026-07-21
2227

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,34 @@ dans `dist/js/` est l’artefact compilé indispensable aux navigateurs ; aucune
6767
JavaScript frontend n’est maintenue hors de `dist/`. Les composants individuels sont
6868
livrés sous `dist/js/components/`.
6969

70+
Depuis la décision D-025, le runtime est aussi livré en ESM, sans changement cassant :
71+
les bundles UMD restent distribués à l’identique (`window.Drake`) et `require('drake.css')`
72+
continue de résoudre `dist/js/drake.js`. Avec un bundler (Vite, esbuild, webpack), la
73+
carte `exports` de `package.json` résout `import` vers l’arborescence de modules préservés
74+
`dist/esm/` ; combinée à la déclaration `sideEffects`, elle permet le tree-shaking : le
75+
bundler n’emporte que les modules réellement consommés. Chaque composant optionnel
76+
s’enregistre à l’import via `import 'drake.css/components/<nom>'` :
77+
78+
```js
79+
import Drake from 'drake.css';
80+
81+
import 'drake.css/components/filter';
82+
```
83+
84+
Sans bundler, les bundles `dist/js/*.esm.js` et `dist/js/components/*.esm.js` (et leurs
85+
variantes `.min`) se chargent en module natif depuis le même CDN, au tag de la release
86+
qui livre l’ESM (`v0.2.0`, véhicule de la décision D-025) :
87+
88+
```html
89+
<script
90+
type="module"
91+
src="https://cdn.jsdelivr.net/gh/Test-bot-cell/Drake-css-framework@v0.2.0/dist/js/drake.esm.js"
92+
></script>
93+
```
94+
95+
Dans tous les cas, l’ESM ne couvre que le JavaScript : les feuilles CSS se chargent
96+
séparément, comme décrit dans [« Chargement recommandé »](#chargement-recommandé).
97+
7098
## Installation locale
7199

72100
La chaîne de référence utilise Node.js 24.18.0 exact et pnpm 11.4.0 :

build/build.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import camelize from 'camelcase';
22
import path from 'node:path';
33
import pLimit from 'p-limit';
4-
import { args, compile, glob } from './util.js';
4+
import { args, compile, compileModules, glob } from './util.js';
55

66
const limit = pLimit(Number(process.env.cpus || 2));
77

@@ -49,9 +49,18 @@ await Promise.all(Object.values(tasks).map((task) => limit(task)));
4949

5050
function getBundleTasks() {
5151
return {
52-
core: () => compile('src/js/drake-core.ts', 'dist/js/drake-core'),
52+
core: () =>
53+
compile('src/js/drake-core.ts', 'dist/js/drake-core', { formats: ['umd', 'es'] }),
5354

54-
drake: () => compile('src/js/drake.ts', 'dist/js/drake'),
55+
drake: () => compile('src/js/drake.ts', 'dist/js/drake', { formats: ['umd', 'es'] }),
56+
57+
// Source ESM des utilitaires pour les composants (D-025 §1) : pas d'UMD,
58+
// `Drake.util` reste porté par les bundles historiques.
59+
'drake-util': () =>
60+
compile('src/js/util/index.ts', 'dist/js/drake-util', { formats: ['es'] }),
61+
62+
// Arborescence de modules préservés (D-025 §1) : le tree-shaking réel.
63+
esm: () => compileModules(['src/js/drake.ts', 'src/js/drake-core.ts'], 'dist/esm'),
5564

5665
tests: async () =>
5766
compile('tests/js/index.ts', 'dist/js/tests/test', {
@@ -74,6 +83,10 @@ async function getComponentTasks() {
7483
name,
7584
external: ['drake', 'drake-util'],
7685
globals: { drake: 'Drake', 'drake-util': 'Drake.util' },
86+
formats: ['umd', 'es'],
87+
// En ESM, les externes se résolvent en voisins de fichiers (D-025 §1) :
88+
// aucune seconde instance de Drake n'est embarquée dans les composants.
89+
esmPaths: { drake: './../drake.esm.js', 'drake-util': './../drake-util.esm.js' },
7790
aliases: { component: path.resolve('src/js/components', `${name}.ts`) },
7891
virtualModules: { 'virtual:name': `'${camelize(name)}'` },
7992
});

build/fork/check-consumer.js

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1921
const execFileAsync = promisify(execFile);
2022
const 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+
3643
const 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+
200273
async function checkRecommendedFiles(installedRoot, installed) {
201274
const files = [...README_RECOMMENDED_FILES, installed.types];
202275
for (const file of files) {

0 commit comments

Comments
 (0)