Acabes de dominar CommonJS: require, module.exports, la memòria cau, l'embolcall de mòdul. És el sistema amb què va néixer Node.js i amb què funciona una part enorme del codi que existeix avui. Però no és el sistema de mòduls de JavaScript.

Mentre Node resolia el problema pel seu compte el 2009, el comitè que estandarditza el llenguatge treballava en una solució oficial. Va arribar el 2015 amb ES2015: els mòduls ES (ESM), amb import i export, definits dins del llenguatge i pensats per funcionar igual al navegador i al servidor. Node va trigar anys a suportar-los de manera estable, i avui conviu amb tots dos.

Aquesta lliçó no és cap catàleg de sintaxi alternativa. La diferència entre tots dos sistemes és profunda: CommonJS és dinàmic i es resol mentre el programa corre; ESM és estàtic i es resol abans d'executar ni una sola línia. D'aquí surten totes les altres diferències: per què les extensions són obligatòries, per què __dirname no existeix, per què CommonJS no pot requerir ESM, i per què només ESM té await de nivell superior.

En acabar sabràs escriure mòduls ES, convertir el que has construït a Escena Viva, fer conviure tots dos sistemes en un mateix projecte coneixent-ne els límits reals, i tindràs un criteri clar sobre què fer servir i quan.

Contingut

  1. La sintaxi dels mòduls ES
  2. La diferència essencial: estàtic contra dinàmic
  3. Taula comparativa completa
  4. Com s'activa ESM a Node
  5. Les extensions són obligatòries
  6. El que no existeix en ESM i com substituir-ho
  7. await de nivell superior
  8. Importació dinàmica: await import()
  9. Interoperabilitat en les dues direccions
  10. La versió ESM del domini d'Escena Viva
  11. Criteri pràctic per al curs

  1. La sintaxi dels mòduls ES

Exportacions amb nom

La forma més habitual, i l'equivalent al nostre module.exports = { ... }:

// src/utils/format.mjs

// Opcio A: exportar a la mateixa declaracio.
export function formatarPreu(centims) {
  const euros = Math.floor(centims / 100);
  const resta = String(centims % 100).padStart(2, '0');
  return `${euros},${resta} EUR`;
}

export function formatarData(dataISO) {
  const data = new Date(dataISO);
  const dia = String(data.getDate()).padStart(2, '0');
  const mes = String(data.getMonth() + 1).padStart(2, '0');
  return `${dia}/${mes}/${data.getFullYear()}`;
}

export const SIMBOL_MONEDA = 'EUR';
// Opcio B: exportar en bloc al final. Equival al module.exports unic
// de CommonJS i te el mateix avantatge: un index de l'API publica.
function formatarPreu(centims) { /* ... */ }
function formatarData(dataISO) { /* ... */ }
const SIMBOL_MONEDA = 'EUR';

export { formatarPreu, formatarData, SIMBOL_MONEDA };

I en importar:

// Importar el que necessites, pel seu nom.
import { formatarPreu, formatarData } from './utils/format.mjs';

// Reanomenar, per evitar collisions.
import { formatarPreu as preu } from './utils/format.mjs';

// Importar-ho TOT en un espai de noms.
import * as format from './utils/format.mjs';
console.log(format.formatarPreu(2500));

// Importar nomes pel seu efecte secundari (rar, pero existeix).
import './configuracio-global.mjs';

Exportació per defecte

Cada mòdul pot tenir una exportació per defecte:

// src/domini/sessio.mjs
export default class Sessio {
  /* ... */
}
// En importar, tu tries el nom: no hi ha claus.
import Sessio from './domini/sessio.mjs';
import ElQueSigui from './domini/sessio.mjs';   // Legal, i confus

Es poden combinar totes dues formes:

// src/domini/esdeveniment.mjs
export default class Esdeveniment { /* ... */ }
export const ESTATS_ESDEVENIMENT = ['esborrany', 'publicat', 'finalitzat'];
import Esdeveniment, { ESTATS_ESDEVENIMENT } from './domini/esdeveniment.mjs';
Amb nom Per defecte
Quantes per mòdul Les que vulguis Una
Sintaxi d'importació import { X } from ... import X from ...
El nom és fix? Sí (tret d'as) No: el tria qui importa
Autocompletat de l'editor Bo Pitjor
Reanomenar en refactoritzar Es propaga Cal revisar-ho a mà

Recomanació per al curs: fes servir exportacions amb nom, no pas export default. És coherent amb el module.exports = { ... } que ja fem servir, l'editor autocompleta millor, i evita que el mateix mòdul aparegui amb cinc noms diferents en cinc fitxers. Moltes guies d'estil professionals (incloses les de Node i de diversos projectes grans) han arribat a la mateixa conclusió.

Reexportar: export * from

L'equivalent de la nostra façana index.js:

// src/domini/index.mjs

// Reexportar TOT el que te nom de cada modul.
export * from './sessio.mjs';
export * from './esdeveniment.mjs';
export * from './gestor-vendes.mjs';

// O ser selectiu, que sol ser millor.
export { Sessio } from './sessio.mjs';
export { Esdeveniment } from './esdeveniment.mjs';
export { GestorDeVendes, LLINDAR_AFORAMENT_BAIX } from './gestor-vendes.mjs';

// Reexportar canviant el nom.
export { Sessio as SessioDEsdeveniment } from './sessio.mjs';

Nota important: export * no reexporta l'exportació per defecte. Si un mòdul té export default, cal reexportar-la explícitament:

export { default as Sessio } from './sessio.mjs';

És una altra raó per preferir exportacions amb nom: les façanes funcionen sense sorpreses.

  1. La diferència essencial: estàtic contra dinàmic

Tota la resta surt d'aquí, així que val la pena entendre-ho bé.

CommonJS és dinàmic

require és una funció normal. S'executa quan l'intèrpret hi arriba, i el seu argument pot ser qualsevol expressió:

// Tot aixo es legal en CommonJS.
const modul = require('./' + nomDelModul + '.js');

if (process.env.MODE === 'produccio') {
  registrador = require('./registrador-produccio.js');
}

for (const nom of ['a', 'b', 'c']) {
  moduls[nom] = require(`./plugins/${nom}.js`);
}

function carregarMandrosament() {
  const pesat = require('./modul-molt-pesat.js');   // Nomes si es crida
  return pesat.processar();
}

Node no pot saber quins mòduls necessita un programa CommonJS sense executar-lo. És flexible i té un preu.

ESM és estàtic

import no és una funció: és una declaració del llenguatge. La seva ruta ha de ser una cadena literal, i les declaracions només poden ser al nivell superior del mòdul.

// TOT aixo es un SyntaxError en ESM.
import modul from './' + nom + '.mjs';            // Ruta no literal

if (produccio) {
  import registrador from './registrador.mjs';    // No dins d'un bloc
}

function carregar() {
  import pesat from './pesat.mjs';                // No dins d'una funcio
}

Gràcies a aquesta rigidesa, el motor pot analitzar tot el graf de dependències abans d'executar res. El procés té tres fases ben separades:

flowchart TD
    subgraph ESM["Mòduls ES: tres fases"]
        A1["<b>1. Construcció</b><br/>Es llegeixen tots els fitxers i<br/>s'analitzen els seus import/export.<br/>Es construeix el graf complet<br/><i>sense executar res</i>"]
        A2["<b>2. Instanciació</b><br/>Es reserva espai per a cada<br/>exportació i es connecten les<br/>referències entre mòduls"]
        A3["<b>3. Avaluació</b><br/>S'executa el codi de cada<br/>mòdul, en ordre de dependències"]
        A1 --> A2 --> A3
    end

    subgraph CJS["CommonJS: una sola fase"]
        B1["<b>Execució</b><br/>S'executa el codi de dalt a baix.<br/>Cada require trobat<br/>carrega i avalua en aquell moment"]
    end

Les conseqüències d'aquesta anàlisi prèvia són molt concretes:

Conseqüència Per què importa
Errors d'importació en temps d'anàlisi Importar alguna cosa que un mòdul no exporta falla abans d'executar, no a mitja petició en producció
Eliminació de codi mort (tree-shaking) Un empaquetador sap quines exportacions no es fan servir i les pot treure. Amb require és impossible saber-ho
Les importacions s'eleven Tots els import es processen abans que qualsevol codi del mòdul, siguin on siguin escrits
Els enllaços són vius Un import no copia el valor: enllaça amb la variable original

Aquest últim punt sorprèn i convé veure'l:

// src/laboratori/comptador.mjs
export let compte = 0;
export function incrementar() {
  compte++;
}
// src/laboratori/usar-comptador.mjs
import { compte, incrementar } from './comptador.mjs';

console.log(compte);   // 0
incrementar();
console.log(compte);   // 1  <-- El valor importat HA CANVIAT

Amb CommonJS, const { compte } = require('./comptador.js') hauria copiat el 0 i no canviaria mai. En ESM, compte és un enllaç viu de només lectura a la variable de l'altre mòdul. La pots llegir i veure'n els canvis, però no li pots assignar res (compte = 5 és un TypeError).

I les importacions s'eleven:

// Aixo funciona en ESM, encara que sembli impossible.
console.log(formatarPreu(2500));            // 25,00 EUR
import { formatarPreu } from './format.mjs';

L'import es processa a la fase 1, molt abans que s'executi el console.log. Funciona, però escriu sempre els import al principi: que el llenguatge ho permeti no ho fa llegible.

  1. Taula comparativa completa

Aspecte CommonJS Mòduls ES
Sintaxi d'importació require('./m.js') import { x } from './m.js'
Sintaxi d'exportació module.exports = {...} export { x } / export default
Extensió de fitxer .js (o .cjs) .mjs, o .js amb "type": "module"
Resolució Dinàmica, en temps d'execució Estàtica, abans d'executar
Ruta d'importació Qualsevol expressió Només cadena literal
Es pot importar condicionalment? Sí No (només amb import() dinàmic)
Extensió en rutes relatives Opcional Obligatòria
Càrrega Síncrona Asíncrona
Valor importat Còpia del module.exports Enllaç viu de només lectura
this al nivell superior module.exports ({}) undefined
__dirname / __filename Disponibles No existeixen (fes servir import.meta.url)
require Disponible No existeix (fes servir createRequire)
import.meta No existeix Disponible
await de nivell superior No Sí
Ordre d'avaluació En trobar el require Dependències primer, en profunditat
Dependències circulars Objecte incomplet ({}) Enllaços sense inicialitzar (ReferenceError)
Mode estricte Opcional Sempre actiu
Carregar JSON require('./d.json') directe Necessita with { type: 'json' }
Eliminació de codi mort No Sí
Compatible amb el navegador No Sí
Pot importar l'altre sistema No pot fer require d'ESM Sí que pot importar CJS

Dues files mereixen comentari extra:

Mode estricte sempre actiu. En ESM no cal 'use strict': està implícit. Això vol dir que assignar a una variable no declarada llança un error, this en una funció solta és undefined, i els duplicats de paràmetres són il·legals. És un bon valor per defecte.

Dependències circulars. En CommonJS rebies un objecte buit i la fallada apareixia més tard, amb un missatge confús. En ESM, gràcies a l'anàlisi estàtica, obtens un ReferenceError: Cannot access 'X' before initialization al punt exacte del problema. És molt millor: un error clar i primerenc en lloc d'un undefined viatjant pel teu codi.

  1. Com s'activa ESM a Node

Node necessita saber si un .js és CommonJS o ESM. Hi ha dues maneres de dir-l'hi.

Opció A: l'extensió del fitxer

Extensió Sistema Quan fer-la servir
.mjs Sempre ESM Un fitxer ESM en un projecte CommonJS
.cjs Sempre CommonJS Un fitxer CommonJS en un projecte ESM
.js Depèn del package.json més proper El cas normal

És explícit i no depèn de res més. Ideal per introduir un fitxer solt de l'altre sistema.

Opció B: el camp type del package.json

{
  "type": "module"
}

Amb aquesta línia, tots els .js del projecte passen a ser mòduls ES. Sense ella (o amb "type": "commonjs", que és el valor per defecte), són CommonJS.

Aquí només ens interessa aquesta línia. El package.json complet —nom, versió, dependències, scripts, exports— és el tema del Mòdul 5. Per ara n'hi ha prou amb saber que un fitxer package.json amb aquest únic camp, a l'arrel del teu projecte, canvia la interpretació de tots els .js.

La regla de resolució

Node busca el package.json més proper cap amunt des del fitxer que carregarà. Això permet barrejar:

escena-viva/
├── package.json                { "type": "module" }
├── src/
│   ├── cataleg.js              -> ESM (pel package.json de l'arrel)
│   ├── domini/
│   │   └── esdeveniment.js     -> ESM
│   └── heretat/
│       ├── package.json        { "type": "commonjs" }
│       └── antic.js            -> CommonJS (pel SEU package.json)
└── eines/
    └── migrar.cjs              -> CommonJS (per l'extensio)

És el mecanisme que permet migrar un projecte gran per parts en lloc de tot de cop.

Comprovar-ho

# Un fitxer .mjs sempre es ESM
echo 'console.log(typeof require);' > prova.mjs
node prova.mjs
# ReferenceError: require is not defined in ES module scope

# El mateix contingut com a .cjs
echo 'console.log(typeof require);' > prova.cjs
node prova.cjs
# function

  1. Les extensions són obligatòries

Aquest és l'entrebanc número u en migrar de CommonJS a ESM.

// CommonJS: les quatre formes funcionen.
require('./sessio');
require('./sessio.js');
require('./domini');          // Troba domini/index.js
require('./domini/index.js');
// ESM: nomes les rutes COMPLETES funcionen.
import { Sessio } from './sessio.js';                 // BE
import { Sessio } from './sessio';                    // ERROR
import { Esdeveniment } from './domini';              // ERROR: no busca index.js
import { Esdeveniment } from './domini/index.js';     // BE
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/joan/escena-viva/src/sessio'
imported from /home/joan/escena-viva/src/cataleg.js
Did you mean to import ./sessio.js?

Node fins i tot et suggereix la correcció, cosa que s'agraeix.

Per què aquesta rigidesa? Perquè ESM està definit per funcionar també al navegador, on un import './sessio' obligaria el navegador a provar sessio, sessio.js, sessio.json… cadascuna amb una petició HTTP d'anada i tornada. Inacceptable. L'estàndard exigeix rutes completes i sense ambigüitat.

Dos matisos importants:

  • La regla només s'aplica a rutes relatives i absolutes. Els paquets de node_modules es continuen important pel seu nom: import express from 'express' és correcte, perquè el mateix paquet declara el seu punt d'entrada.
  • index.js no és especial en ESM. Cal escriure la ruta completa. Les nostres façanes passen a ser import { Esdeveniment } from './domini/index.js'.

Consell pràctic: escriu sempre les extensions també als teus fitxers CommonJS —com hem fet en tota la lliçó anterior—. El dia que migris, la meitat de la feina ja estarà feta.

  1. El que no existeix en ESM i com substituir-ho

Les cinc variables de l'embolcall de mòdul que vas aprendre a la lliçó anterior no existeixen en ESM. No hi ha embolcall: un mòdul ES és un mòdul de debò, definit al llenguatge.

No existeix Substitut
__dirname path.dirname(fileURLToPath(import.meta.url))
__filename fileURLToPath(import.meta.url)
require createRequire(import.meta.url) o await import()
module.exports export
exports export
require.main === module import.meta.url === pathToFileURL(process.argv[1]).href
require.cache No hi ha API pública equivalent

import.meta

ESM aporta un objecte propi amb metadades del mòdul actual:

// src/laboratori/metadades.mjs
console.log(import.meta.url);
// file:///home/joan/escena-viva/src/laboratori/metadades.mjs

Fixa't que és una URL, no pas una ruta del sistema de fitxers. És coherent amb l'estàndard (al navegador seria https://...), però significa que cal convertir-la abans de fer-la servir amb fs o path.

Recuperar __dirname i __filename

// src/utils/rutes.mjs
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

// Equivalents exactes de les variables de CommonJS.
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

console.log(__filename);   // /home/joan/escena-viva/src/utils/rutes.mjs
console.log(__dirname);    // /home/joan/escena-viva/src/utils

// Us habitual: una ruta fiable a les dades.
const RUTA_DADES = join(__dirname, '..', '..', 'dades', 'esdeveniments.json');

En versions recents de Node (20.11 i posteriors) hi ha una drecera:

const __dirname = import.meta.dirname;    // Directament, sense conversio
const __filename = import.meta.filename;

Comprova la teva versió amb node -v abans de fer-ho servir; si el teu projecte ha de funcionar en versions anteriors, fes servir la forma amb fileURLToPath.

Recuperar require

Per als casos en què necessites un mòdul CommonJS que no es deixa importar bé:

// src/laboratori/usar-require.mjs
import { createRequire } from 'node:module';

const require = createRequire(import.meta.url);

// Ara funciona com en CommonJS, inclos el JSON.
const cataleg = require('../dades/esdeveniments.json');
const paquetAntic = require('paquet-nomes-commonjs');

console.log(`${cataleg.length} esdeveniments`);

createRequire necessita saber des d'on resoldre les rutes relatives, i per això rep import.meta.url.

Detectar si ets el programa principal

// src/informes/ocupacio.mjs
import { pathToFileURL } from 'node:url';

const esProgramaPrincipal = import.meta.url === pathToFileURL(process.argv[1]).href;

if (esProgramaPrincipal) {
  // Nomes amb: node src/informes/ocupacio.mjs
  executarInforme();
}

Menys elegant que require.main === module, però equivalent. A Node 20.11+ existeix també import.meta.main en algunes configuracions; consulta la documentació de la teva versió.

Importar JSON

// Sintaxi d'atributs d'importacio (Node 20.10+ / 22+).
import cataleg from '../dades/esdeveniments.json' with { type: 'json' };

console.log(cataleg.length);

L'atribut with { type: 'json' } és obligatori i és una mesura de seguretat: evita que un servidor maliciós torni JavaScript on esperaves dades. Com a alternativa, sempre pots llegir el fitxer amb fs, que és el que farem a Escena Viva a partir del Mòdul 3.

  1. await de nivell superior

L'avantatge exclusiu d'ESM, i un dels més còmodes.

// src/laboratori/carrega.mjs
import { readFile } from 'node:fs/promises';

// await directament, sense embolcallar-ho en cap funcio async.
const contingut = await readFile('dades/esdeveniments.json', 'utf8');
const cataleg = JSON.parse(contingut);

console.log(`Carregats ${cataleg.length} esdeveniments`);

Compara-ho amb el que calia escriure en CommonJS:

// CommonJS: cal una funcio embolcall i el seu .catch.
const { readFile } = require('node:fs/promises');

async function principal() {
  const contingut = await readFile('dades/esdeveniments.json', 'utf8');
  const cataleg = JSON.parse(contingut);
  console.log(`Carregats ${cataleg.length} esdeveniments`);
}

principal().catch((error) => {
  console.error(error.message);
  process.exitCode = 1;
});

Els seus usos naturals:

// 1. Inicialitzacio asincrona abans d'exportar.
const configuracio = JSON.parse(await readFile('./config.json', 'utf8'));
export { configuracio };

// 2. Eleccio de dependencia en temps d'execucio.
const registrador = process.env.NODE_ENV === 'produccio'
  ? await import('./registrador-produccio.mjs')
  : await import('./registrador-desenvolupament.mjs');

// 3. Recursos que han d'estar a punt abans que ningu faci servir el modul.
const connexio = await connectarABaseDeDades();
export { connexio };

Com funciona: un mòdul amb await de nivell superior es converteix en un mòdul asíncron. Els mòduls que l'importin esperaran que acabi abans d'executar-se. És potent i té un cost: si aquest await triga cinc segons, tots els importadors esperen cinc segons. Fes-lo servir per a inicialització real, no com una drecera còmoda en qualsevol fitxer.

I un advertiment: si l'await de nivell superior falla, el mòdul sencer falla en carregar-se i l'aplicació no arrenca. Embolcalla'l en try/catch si vols degradar amb elegància.

  1. Importació dinàmica: await import()

import() com a funció és la via d'escapament que torna la flexibilitat de require sense renunciar a l'anàlisi estàtica. Torna una promesa que es compleix amb l'espai de noms del mòdul.

// Funciona tant en ESM com en CommonJS.
const modul = await import('./domini/esdeveniment.mjs');
console.log(modul.Esdeveniment);

// Amb desestructuracio.
const { Esdeveniment } = await import('./domini/esdeveniment.mjs');

Els seus tres usos legítims:

8.1 Càrrega condicional

// src/laboratori/carrega-condicional.mjs

const mode = process.env.NODE_ENV ?? 'desenvolupament';

// La ruta pot ser una expressio: aqui si.
const { registrador } = await import(`./registradors/${mode}.mjs`);

registrador.info('Escena Viva arrencant');

8.2 Càrrega mandrosa de mòduls pesats

// El generador de PDF de les entrades pesa molt i poques vegades es fa servir.
// No el volem carregar a l'arrencada de cada proces.

export async function generarPdfEntrada(entrada) {
  const { crearPdf } = await import('./pdf/generador.mjs');
  return crearPdf(entrada);
}

Es carrega la primera vegada que algú genera un PDF, i a partir d'aquí queda a la memòria cau.

8.3 Carregar CommonJS des d'ESM (i des de CommonJS, ESM)

És el pont d'interoperabilitat, i el veiem a l'apartat següent.

import estàtic import() dinàmic
Ruta Cadena literal Qualsevol expressió
On pot aparèixer Nivell superior A qualsevol lloc
Torna Enllaços vius Una promesa
Permet eliminar codi mort? Sí No
Funciona en CommonJS? No Sí

No abusis d'import(). Cada importació dinàmica és una dependència que les eines no poden analitzar. Fes-lo servir quan tinguis una raó (condicionalitat, pes, interoperabilitat), no per costum.

  1. Interoperabilitat en les dues direccions

Aquí hi ha la part que genera més frustració en projectes reals, i les regles no són simètriques.

flowchart LR
    ESM["Mòdul ES<br/>(.mjs)"]
    CJS["Mòdul CommonJS<br/>(.cjs)"]

    ESM -->|"import ... from<br/><b>SÍ, amb matisos</b>"| CJS
    CJS -->|"require<br/><b>NO</b>"| ESM
    CJS -.->|"await import()<br/><b>SÍ</b>"| ESM

9.1 ESM important CommonJS: sí, amb matisos

// src/utils/format.cjs  (CommonJS)
function formatarPreu(centims) { /* ... */ }
function formatarData(dataISO) { /* ... */ }

module.exports = { formatarPreu, formatarData };
// src/cataleg.mjs  (ESM)

// El module.exports complet arriba com a exportacio PER DEFECTE.
import format from './utils/format.cjs';
console.log(format.formatarPreu(2500));   // Sempre funciona

// Les exportacions amb nom funcionen... si Node aconsegueix detectar-les.
import { formatarPreu } from './utils/format.cjs';   // Sol funcionar

La regla exacta: el module.exports d'un mòdul CommonJS sempre arriba com a exportació per defecte. A més, Node executa un analitzador estàtic (cjs-module-lexer) sobre el fitxer per intentar detectar les exportacions amb nom i oferir-les també. Aquesta anàlisi és sintàctica, no executa el codi, així que falla tan bon punt els exports es construeixen de manera dinàmica:

// CommonJS amb exports dinamics: l'analitzador NO els pot detectar.
const funcions = { formatarPreu, formatarData };
for (const [nom, fn] of Object.entries(funcions)) {
  module.exports[nom] = fn;
}
// Des d'ESM:
import { formatarPreu } from './format.cjs';
// SyntaxError: The requested module './format.cjs' does not provide
// an export named 'formatarPreu'

La solució universal i sempre segura:

// Importa l'objecte complet per defecte i desestructura despres.
import format from './utils/format.cjs';
const { formatarPreu, formatarData } = format;

Si un paquet d'npm et dona aquest error en importar-lo amb claus, aquesta és la resposta.

9.2 CommonJS requerint ESM: no

// src/antic.cjs
const { Esdeveniment } = require('./domini/esdeveniment.mjs');
Error [ERR_REQUIRE_ESM]: require() of ES Module
/home/joan/escena-viva/src/domini/esdeveniment.mjs not supported.
Instead change the require of esdeveniment.mjs to a dynamic import() which is
available in all CommonJS modules.

La raó és estructural, no cap caprici: require és síncron —torna el valor immediatament— i la càrrega d'ESM és asíncrona, perquè hi pot haver await de nivell superior en qualsevol punt del graf de dependències. No hi ha manera que una funció síncrona torni el resultat d'un procés asíncron.

La solució és la importació dinàmica, que sí que funciona en CommonJS:

// src/antic.cjs
async function principal() {
  const { Esdeveniment } = await import('./domini/esdeveniment.mjs');

  const esdeveniment = new Esdeveniment({ id: 'evt-001', titol: 'Concierto de Otono' });
  console.log(esdeveniment.toString());
}

principal().catch((error) => {
  console.error(error.message);
  process.exitCode = 1;
});

El preu: la funció que ho fa s'ha de tornar asíncrona, i aquesta asincronia es propaga cap amunt. És el que es coneix com el problema de la «coloració de funcions», i és la causa que migrar un projecte gran de CommonJS a ESM sigui més laboriós del que sembla.

Nota sobre versions recents. Node 22 va introduir require() de mòduls ES síncrons (sense await de nivell superior) darrere d'un indicador experimental, i s'ha anat estabilitzant en versions posteriors. És una millora real per a la migració, però no la donis per feta: depèn de la versió i el mòdul importat no pot contenir await de nivell superior en cap punt del seu graf. La regla general que has de tenir al cap continua sent la de dalt.

9.3 Resum de la interoperabilitat

Des de Cap a Funciona? Com
ESM CommonJS Sí import x from './m.cjs' (per defecte: sempre)
ESM CommonJS, exports amb nom Gairebé sempre Depèn de l'anàlisi estàtica; si falla, importa per defecte
ESM ESM Sí import { x } from './m.mjs'
CommonJS ESM amb require No ERR_REQUIRE_ESM (tret de casos recents i limitats)
CommonJS ESM amb import() Sí const m = await import('./m.mjs')
CommonJS CommonJS Sí require('./m.cjs')

  1. La versió ESM del domini d'Escena Viva

Traduirem el que vas construir a la lliçó anterior. Veuràs que la lògica no canvia gens ni mica: només canvien les línies d'entrada i sortida.

src/utils/format.js

// ---------- CommonJS ----------
function formatarPreu(centims) {
  const euros = Math.floor(centims / 100);
  const resta = String(centims % 100).padStart(2, '0');
  return `${euros},${resta} EUR`;
}

function formatarData(dataISO) { /* ... */ }
function generarCodiEntrada(any, sequencia) { /* ... */ }

module.exports = { formatarPreu, formatarData, generarCodiEntrada };
// ---------- Mòduls ES ----------
export function formatarPreu(centims) {
  const euros = Math.floor(centims / 100);
  const resta = String(centims % 100).padStart(2, '0');
  return `${euros},${resta} EUR`;
}

export function formatarData(dataISO) { /* ... */ }
export function generarCodiEntrada(any, sequencia) { /* ... */ }

Sense línia final: cada funció s'exporta on es declara.

src/domini/sessio.js

// ---------- CommonJS ----------
const { formatarPreu, formatarData } = require('../utils/format.js');

class Sessio {
  #venudes = 0;
  /* ... el cos sencer, sense ni un sol canvi ... */
}

module.exports = { Sessio };
// ---------- Mòduls ES ----------
import { formatarPreu, formatarData } from '../utils/format.js';

export class Sessio {
  #venudes = 0;
  /* ... el cos sencer, sense ni un sol canvi ... */
}

Els camps privats, els getters, toJSON, la validació: tot idèntic. El sistema de mòduls no toca la lògica.

src/domini/gestor-vendes.js

// ---------- CommonJS ----------
const EventEmitter = require('node:events');

const LLINDAR_AFORAMENT_BAIX = 0.10;

class GestorDeVendes extends EventEmitter { /* ... */ }

module.exports = { GestorDeVendes, LLINDAR_AFORAMENT_BAIX };
// ---------- Mòduls ES ----------
import { EventEmitter } from 'node:events';

export const LLINDAR_AFORAMENT_BAIX = 0.10;

export class GestorDeVendes extends EventEmitter { /* ... */ }

Un detall real que mereix atenció: en CommonJS escrivíem require('node:events') i fèiem servir el resultat directament com a classe, perquè el mòdul events exporta la classe com el seu module.exports. En ESM, la forma recomanada és la importació amb nom import { EventEmitter } from 'node:events'. Totes dues funcionen (el mòdul ofereix les dues), però la que té nom és més explícita i és la que veuràs a la documentació moderna.

src/domini/index.js

// ---------- CommonJS ----------
const { Sessio } = require('./sessio.js');
const { Esdeveniment } = require('./esdeveniment.js');
const { GestorDeVendes, LLINDAR_AFORAMENT_BAIX } = require('./gestor-vendes.js');

module.exports = { Sessio, Esdeveniment, GestorDeVendes, LLINDAR_AFORAMENT_BAIX };
// ---------- Mòduls ES ----------
export { Sessio } from './sessio.js';
export { Esdeveniment } from './esdeveniment.js';
export { GestorDeVendes, LLINDAR_AFORAMENT_BAIX } from './gestor-vendes.js';

La versió ESM és més curta i més clara: reexporta directament, sense necessitat d'importar primer per tornar a exportar després. És una de les millores genuïnes de la sintaxi.

src/cataleg-dades.js

// ---------- CommonJS ----------
const cataleg = [ /* els 3 esdeveniments */ ];

function obtenirCataleg() {
  return structuredClone(cataleg);
}

function obtenirEsdevenimentPerId(id) { /* ... */ }

module.exports = { obtenirCataleg, obtenirEsdevenimentPerId };
// ---------- Mòduls ES ----------
// L'array queda PRIVAT: en no exportar-lo, ningu de fora no el pot tocar.
const cataleg = [ /* els 3 esdeveniments */ ];

export function obtenirCataleg() {
  return structuredClone(cataleg);
}

export function obtenirEsdevenimentPerId(id) { /* ... */ }

src/cataleg.js

// ---------- CommonJS ----------
const { obtenirCataleg } = require('./cataleg-dades.js');
const { Esdeveniment } = require('./domini');              // Sense extensio: valid
const { formatarPreu } = require('./utils/format.js');

function principal() { /* ... */ }

if (require.main === module) {
  principal();
}

module.exports = { llegirOpcions, principal };
// ---------- Mòduls ES ----------
import { pathToFileURL } from 'node:url';
import { obtenirCataleg } from './cataleg-dades.js';
import { Esdeveniment } from './domini/index.js';          // Extensio OBLIGATORIA
import { formatarPreu } from './utils/format.js';

export function principal() { /* ... */ }

// Equivalent de require.main === module
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  principal();
}

Els tres canvis reals d'aquest fitxer, i són els tres que veuràs sempre en migrar:

  1. ./domini → ./domini/index.js: l'extensió i l'index.js explícit.
  2. require.main === module → la comparació d'URL.
  3. module.exports al final → export a cada declaració.

La versió ESM que aprofita l'await de nivell superior

I aquí un avantatge real, no pas cosmètic. Avançant el que farem al Mòdul 3, així quedarà la càrrega del catàleg des de disc:

// src/cataleg-dades.mjs  (avancament del modul 3)
import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const __dirname = dirname(fileURLToPath(import.meta.url));
const RUTA_DADES = join(__dirname, '..', 'dades', 'esdeveniments.json');

// await de nivell superior: el cataleg es carrega UN COP, en importar el modul.
// Qui importi aquest modul rebra les dades ja a punt.
const cataleg = JSON.parse(await readFile(RUTA_DADES, 'utf8'));

export function obtenirCataleg() {
  return structuredClone(cataleg);
}

export function obtenirEsdevenimentPerId(id) {
  const esdeveniment = cataleg.find((e) => e.id === id);
  return esdeveniment ? structuredClone(esdeveniment) : undefined;
}

Això no té equivalent en CommonJS. Allà hauries de triar entre una lectura síncrona bloquejant (readFileSync) o exportar una promesa que cada consumidor hauria d'esperar. Amb ESM, la inicialització asíncrona passa a la càrrega del mòdul, transparent per a qui el fa servir.

  1. Criteri pràctic per al curs

Què farem servir

A la resta del curs continuarem fent servir CommonJS com a sistema principal, i aquestes són les raons:

  1. És el que et trobaràs. Milions de projectes, incomptables tutorials i una part enorme d'npm continuen sent CommonJS. Saber-lo llegir no és opcional.
  2. Menys fricció per aprendre. require.main === module és més simple que comparar URLs, __dirname hi és sense cerimònies, i no cal pensar en interoperabilitat mentre aprens fs o Express.
  3. Express i bona part de l'ecosistema clàssic documenten els seus exemples en CommonJS.

Però cada vegada que un tema tingui una versió ESM rellevant, la mostrarem, i al Mòdul 12 farem la migració completa d'Escena Viva com a exercici de tancament.

Què fer servir als teus projectes nous

Situació Recomanació
Projecte nou des de zero ESM. És l'estàndard del llenguatge i el futur
Projecte CommonJS existent i gran Quedar-se en CommonJS, o migrar per parts amb .mjs
Biblioteca que publicaràs a npm Publicar tots dos formats (el camp exports del Mòdul 5)
Codi que també corre al navegador ESM, sense discussió
Script ràpid amb await al nivell superior ESM (o un .mjs solt)
Eina de configuració d'un altre projecte El que aquell projecte esperi

Per què l'ecosistema conviu amb tots dos

No és cap accident ni mandra col·lectiva. Hi ha raons de fons:

  • La compatibilitat cap enrere és un valor. Node no pot trencar milions de projectes en producció d'un dia per l'altre.
  • La migració és contagiosa. Si el teu mòdul passa a ESM, els qui el consumien des de CommonJS ja no poden fer servir require. La pressió es propaga cap amunt per l'arbre de dependències.
  • Moltes eines esperen CommonJS. Fitxers de configuració, complements i alguns executors de proves continuen assumint-ho.
  • La solució de les biblioteques és publicar tots dos formats (dual package), a canvi d'una configuració més complexa i d'un risc real: carregar dues còpies de la mateixa biblioteca, una per cada sistema, amb estats independents.

La transició dura gairebé una dècada i li queden anys. L'habilitat professional no és triar un bàndol: és saber treballar amb tots dos i reconèixer a l'instant quin tens al davant.

Errors Comuns i Consells

Error 1: ometre l'extensió en un import relatiu. ERR_MODULE_NOT_FOUND. És l'entrebanc número u en migrar. Sempre ./sessio.js.

Error 2: esperar que ./domini trobi index.js. En ESM no hi ha resolució de carpeta. Escriu ./domini/index.js.

Error 3: fer servir __dirname en un fitxer ESM. ReferenceError. Fes servir import.meta.dirname o fileURLToPath(import.meta.url).

Error 4: require() d'un mòdul ES. ERR_REQUIRE_ESM. Fes servir await import() i assumeix que la funció passa a ser asíncrona.

Error 5: importar amb claus d'un paquet CommonJS i que falli. L'analitzador no va detectar les exportacions amb nom. Importa per defecte i desestructura.

Error 6: afegir "type": "module" a un projecte existent sense més. Tots els .js canvien de sistema alhora i tot es trenca. Migra per parts amb .mjs, o reanomena a .cjs el que hagi de continuar igual.

Error 7: await de nivell superior en un mòdul importat per molts altres. Tots esperen que acabi. Reserva'l per a inicialització real.

Error 8: abusar de l'import() dinàmic. Perds l'anàlisi estàtica i l'eliminació de codi mort. Només amb una raó concreta.

Consell 1: escriu les extensions també en CommonJS. Quan migris, aquesta feina ja estarà feta.

Consell 2: fes servir exportacions amb nom, no export default. Millor autocompletat, noms coherents i façanes sense sorpreses.

Consell 3: aprèn a reconèixer el sistema en tres segons. Veus import? ESM. Veus require? CommonJS. És un .js? Mira el package.json més proper.

Consell 4: quan un paquet d'npm et doni problemes d'importació, mira el seu package.json. El camp exports (Mòdul 5) i el camp type et diuen exactament què ofereix.

Exercicis

Exercici 1: migrar el domini a ESM

Crea una còpia del projecte a escena-viva-esm/ i migra a mòduls ES tot el que vas construir a la lliçó anterior:

  1. Un package.json amb únicament { "type": "module" }.
  2. src/utils/format.js, src/domini/sessio.js, src/domini/esdeveniment.js, src/domini/gestor-vendes.js, src/domini/index.js, src/cataleg-dades.js i src/cataleg.js.
  3. Totes les rutes relatives amb la seva extensió, i ./domini/index.js explícit.
  4. require.main === module substituït per la comparació amb import.meta.url.
  5. src/informes/ocupacio.js migrat i executable directament.

Verifica que node src/cataleg.js --taula produeix exactament la mateixa sortida que la versió CommonJS, amb els mateixos totals (3000 d'aforament, 1811 venudes, 1189 lliures). Anota quants canvis has hagut de fer i de quin tipus.

Exercici 2: interoperabilitat en les dues direccions

Crea escena-viva-mixt/ amb un projecte que demostri experimentalment les regles de l'apartat 9:

  1. src/utils/format.cjs — CommonJS, amb module.exports = { formatarPreu, formatarData }.
  2. src/utils/llegat-dinamic.cjs — CommonJS que construeix els seus exports dinàmicament en un bucle.
  3. src/domini/sessio.mjs — ESM que importa format.cjs (que funcioni) i intenta importar amb claus de llegat-dinamic.cjs (que falli), amb la solució aplicada.
  4. src/informe.cjs — CommonJS que necessita la classe Sessio del fitxer ESM. Demostra primer que require falla (captura l'error i mostra'n el code) i després resol-ho amb await import().
  5. Un README.md amb una taula de quina combinació funciona, quina no i per què.

Exercici 3: el carregador de catàleg amb await de nivell superior

Escriu, en un projecte ESM, src/cataleg-dades.mjs que aprofiti de debò allò exclusiu d'ESM:

  1. Carregui dades/esdeveniments.json amb readFile de node:fs/promises i await de nivell superior, fent servir import.meta.url per construir una ruta fiable.
  2. Tracti la fallada de lectura: si el fitxer no existeix, que registri un avís clar per stderr i continuï amb un catàleg buit en lloc d'impedir l'arrencada de l'aplicació.
  3. Exporti obtenirCataleg(), obtenirEsdevenimentPerId(id) i obtenirSessio(sessioId), totes tornant còpies.
  4. Exporti també una constant CARREGAT_EL amb la marca de temps ISO del moment de la càrrega, i demostri —important-lo des de tres fitxers diferents— que el valor és el mateix als tres: la memòria cau de mòduls també existeix en ESM.
  5. Un src/principal.mjs que faci servir tot l'anterior amb await de nivell superior, sense cap funció principal() embolcalladora.

Respon: què passaria si l'await de nivell superior trigués 3 segons? I si llancés una excepció no capturada?

Solucions

Solució 1

// escena-viva-esm/package.json
{
  "type": "module"
}
// src/utils/format.js
export function formatarPreu(centims) {
  const euros = Math.floor(centims / 100);
  const resta = String(centims % 100).padStart(2, '0');
  return `${euros},${resta} EUR`;
}

export function formatarData(dataISO) {
  const data = new Date(dataISO);
  const dia = String(data.getDate()).padStart(2, '0');
  const mes = String(data.getMonth() + 1).padStart(2, '0');
  const any = data.getFullYear();
  const hora = String(data.getHours()).padStart(2, '0');
  const minut = String(data.getMinutes()).padStart(2, '0');
  return `${dia}/${mes}/${any} ${hora}:${minut}`;
}

export function generarCodiEntrada(any, sequencia) {
  return `EV-${any}-${String(sequencia).padStart(6, '0')}`;
}
// src/domini/sessio.js
import { formatarPreu, formatarData } from '../utils/format.js';

export class Sessio {
  #venudes = 0;

  constructor({ id, dataHora, aforament, venudes = 0, preuCentims }) {
    this.id = id;
    this.dataHora = dataHora;
    this.aforament = aforament;
    this.preuCentims = preuCentims;
    this.#venudes = venudes;
  }

  get venudes() { return this.#venudes; }
  get lliures() { return this.aforament - this.#venudes; }
  get ocupacio() { return Math.round((this.#venudes / this.aforament) * 100); }
  get exhaurida() { return this.lliures === 0; }
  get preuEuros() { return (this.preuCentims / 100).toFixed(2); }
  get recaptacioCentims() { return this.#venudes * this.preuCentims; }

  vendre(quantitat = 1) {
    if (!Number.isInteger(quantitat) || quantitat < 1) {
      const error = new Error('La quantitat ha de ser un enter positiu');
      error.codi = 'QUANTITAT_INVALIDA';
      throw error;
    }
    if (quantitat > this.lliures) {
      const error = new Error(`Aforament insuficient a ${this.id}: en queden ${this.lliures}`);
      error.codi = 'AFORAMENT_INSUFICIENT';
      throw error;
    }
    this.#venudes += quantitat;
    return this.#venudes;
  }

  descriure() {
    return (
      `${formatarData(this.dataHora)}  ${formatarPreu(this.preuCentims)}  ` +
      `${this.lliures}/${this.aforament} lliures  (${this.ocupacio}% ocupat)` +
      (this.exhaurida ? '  [EXHAURIDA]' : '')
    );
  }

  toJSON() {
    return {
      id: this.id,
      dataHora: this.dataHora,
      aforament: this.aforament,
      venudes: this.#venudes,
      preuCentims: this.preuCentims
    };
  }
}
// src/domini/index.js
export { Sessio } from './sessio.js';
export { Esdeveniment } from './esdeveniment.js';
export { GestorDeVendes, LLINDAR_AFORAMENT_BAIX } from './gestor-vendes.js';
// src/cataleg.js  (nomes les parts que canvien)
import { pathToFileURL } from 'node:url';
import { obtenirCataleg } from './cataleg-dades.js';
import { Esdeveniment } from './domini/index.js';
import { formatarPreu } from './utils/format.js';

export function llegirOpcions(parametres) { /* identic */ }

export function principal() { /* identic */ }

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  principal();
}
node src/cataleg.js --taula
# Sortida IDENTICA a la versio CommonJS
# 3 esdeveniments | aforament 3000 | venudes 1811 | lliures 1189 | recaptacio 62298,00 EUR

Inventari de canvis. Migrar set fitxers va exigir exactament quatre tipus de canvi, i cap no va tocar la lògica:

Tipus de canvi Quantes vegades Detall
const {...} = require(...) → import {...} from ... 9 Mecànic
module.exports = {...} → export a la declaració 7 Un per fitxer
require('./domini') → './domini/index.js' 2 La resolució de carpeta no existeix
require.main === module → comparació d'URL 2 Més import { pathToFileURL }

Zero canvis en classes, camps privats, getters, validacions, càlculs o format. És la demostració pràctica que el sistema de mòduls és infraestructura: canviar-lo no hauria de tocar la teva lògica de negoci, i si la toca, és que estaven massa acoblats.

Solució 2

// src/utils/format.cjs
// CommonJS amb exports ESTATICS: l'analitzador de Node els detecta.
function formatarPreu(centims) {
  const euros = Math.floor(centims / 100);
  return `${euros},${String(centims % 100).padStart(2, '0')} EUR`;
}

function formatarData(dataISO) {
  return dataISO.slice(0, 10).split('-').reverse().join('/');
}

module.exports = { formatarPreu, formatarData };
// src/utils/llegat-dinamic.cjs
// CommonJS amb exports DINAMICS: l'analitzador NO els pot detectar.
function calcularOcupacio(sessio) {
  return Math.round((sessio.venudes / sessio.aforament) * 100);
}

function calcularLliures(sessio) {
  return sessio.aforament - sessio.venudes;
}

// Construccio dinamica: nomes se sap en temps d'execucio.
const funcions = { calcularOcupacio, calcularLliures };
for (const [nom, fn] of Object.entries(funcions)) {
  module.exports[nom] = fn;
}
// src/domini/sessio.mjs
// ESM important CommonJS en les seves dues variants.

// 1. Exports estatics: la importacio amb nom FUNCIONA.
import { formatarPreu } from '../utils/format.cjs';

// 2. Exports dinamics: la que te nom FALLARIA.
//    import { calcularOcupacio } from '../utils/llegat-dinamic.cjs';
//    SyntaxError: does not provide an export named 'calcularOcupacio'
//
//    Solucio universal: importar per defecte i desestructurar.
import llegat from '../utils/llegat-dinamic.cjs';
const { calcularOcupacio, calcularLliures } = llegat;

export class Sessio {
  constructor({ id, aforament, venudes = 0, preuCentims }) {
    this.id = id;
    this.aforament = aforament;
    this.venudes = venudes;
    this.preuCentims = preuCentims;
  }

  descriure() {
    return (
      `${this.id}  ${formatarPreu(this.preuCentims)}  ` +
      `${calcularLliures(this)} lliures  (${calcularOcupacio(this)}%)`
    );
  }
}
// src/informe.cjs
// CommonJS que necessita una classe definida en un modul ES.

// 1. Demostracio que require FALLA.
try {
  const { Sessio } = require('./domini/sessio.mjs');
  console.log("Aixo no s'imprimeix:", Sessio);
} catch (error) {
  console.error(`require ha fallat amb codi: ${error.code}`);
  console.error(`  ${error.message.split('\n')[0]}`);
}

// 2. Solucio amb importacio dinamica.
async function principal() {
  const { Sessio } = await import('./domini/sessio.mjs');

  const sessio = new Sessio({
    id: 'ses-002-1', aforament: 120, venudes: 118, preuCentims: 1800
  });

  console.log('');
  console.log('Amb await import() SI que funciona:');
  console.log(`  ${sessio.descriure()}`);
}

principal().catch((error) => {
  console.error(`Fallada: ${error.message}`);
  process.exitCode = 1;
});
node src/informe.cjs
require ha fallat amb codi: ERR_REQUIRE_ESM
  require() of ES Module /home/joan/escena-viva-mixt/src/domini/sessio.mjs not supported.

Amb await import() SI que funciona:
  ses-002-1  18,00 EUR  2 lliures  (98%)

Taula del README.md:

Des de Cap a Sintaxi Funciona? Motiu
.mjs .cjs amb exports estàtics import { x } from Sí L'analitzador detecta els noms
.mjs .cjs amb exports dinàmics import { x } from No L'analitzador és sintàctic, no executa el codi
.mjs .cjs amb exports dinàmics import m from + desestructurar Sí El module.exports complet sempre arriba com a default
.cjs .mjs require() No require és síncron; la càrrega d'ESM és asíncrona
.cjs .mjs await import() Sí Torna una promesa, compatible amb la càrrega asíncrona

Solució 3

// src/cataleg-dades.mjs
// Carrega del cataleg amb await de nivell superior. Exclusiu d'ESM.

import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const __dirname = dirname(fileURLToPath(import.meta.url));
const RUTA_DADES = join(__dirname, '..', 'dades', 'esdeveniments.json');

// 1 i 2. Carrega amb await de nivell superior i degradacio elegant davant la fallada.
let cataleg = [];

try {
  const contingut = await readFile(RUTA_DADES, 'utf8');
  cataleg = JSON.parse(contingut);
  console.error(`[cataleg] carregats ${cataleg.length} esdeveniments des de ${RUTA_DADES}`);
} catch (error) {
  if (error.code === 'ENOENT') {
    console.error(`[cataleg] AVIS: no s'ha trobat ${RUTA_DADES}. Cataleg buit.`);
  } else if (error instanceof SyntaxError) {
    console.error(`[cataleg] AVIS: el fitxer no es JSON valid (${error.message}).`);
  } else {
    console.error(`[cataleg] AVIS: fallada de lectura (${error.message}).`);
  }
  // No el rellancem: l'aplicacio arrenca amb el cataleg buit.
}

// 4. Marca de temps del moment de la carrega.
export const CARREGAT_EL = new Date().toISOString();

// 3. Acces a les dades, sempre amb copies.
export function obtenirCataleg() {
  return structuredClone(cataleg);
}

export function obtenirEsdevenimentPerId(id) {
  const esdeveniment = cataleg.find((e) => e.id === id);
  return esdeveniment ? structuredClone(esdeveniment) : undefined;
}

export function obtenirSessio(sessioId) {
  for (const esdeveniment of cataleg) {
    const sessio = esdeveniment.sessions.find((s) => s.id === sessioId);
    if (sessio) {
      return structuredClone({ esdeveniment: { id: esdeveniment.id, titol: esdeveniment.titol }, sessio });
    }
  }
  return undefined;
}
// src/consumidor-a.mjs
import { CARREGAT_EL, obtenirCataleg } from './cataleg-dades.mjs';

export function informar() {
  return { modul: 'consumidor-a', carregatEl: CARREGAT_EL, esdeveniments: obtenirCataleg().length };
}
// src/consumidor-b.mjs
import { CARREGAT_EL, obtenirEsdevenimentPerId } from './cataleg-dades.mjs';

export function informar() {
  const esdeveniment = obtenirEsdevenimentPerId('evt-002');
  return { modul: 'consumidor-b', carregatEl: CARREGAT_EL, titol: esdeveniment?.titol ?? '(sense dades)' };
}
// src/principal.mjs
// Sense funcio embolcalladora: await de nivell superior a tot el fitxer.

import { setTimeout as dormir } from 'node:timers/promises';
import { CARREGAT_EL, obtenirCataleg, obtenirSessio } from './cataleg-dades.mjs';
import { informar as informarA } from './consumidor-a.mjs';
import { informar as informarB } from './consumidor-b.mjs';

const esdeveniments = obtenirCataleg();

console.log('CATALEG');
console.table(esdeveniments.map((e) => ({
  id: e.id,
  titol: e.titol,
  sala: e.sala,
  sessions: e.sessions.length,
  aforament: e.sessions.reduce((t, s) => t + s.aforament, 0),
  venudes: e.sessions.reduce((t, s) => t + s.venudes, 0)
})));

const trobada = obtenirSessio('ses-002-1');
console.log('');
console.log(`Sessio ses-002-1: ${trobada.esdeveniment.titol}, ` +
  `${trobada.sessio.aforament - trobada.sessio.venudes} lliures`);

// Pausa amb await de nivell superior: impossible en CommonJS.
await dormir(50);

// 4. La marca de temps es la MATEIXA als tres moduls.
console.log('');
console.log('MEMORIA CAU DE MODULS EN ESM');
console.table([
  { modul: 'principal', carregatEl: CARREGAT_EL, dada: `${esdeveniments.length} esdeveniments` },
  { ...informarA(), dada: `${informarA().esdeveniments} esdeveniments` },
  { ...informarB(), dada: informarB().titol }
]);

const totesIguals = CARREGAT_EL === informarA().carregatEl && CARREGAT_EL === informarB().carregatEl;
console.log('');
console.log(`Mateixa marca de temps als tres moduls? ${totesIguals}`);
[cataleg] carregats 3 esdeveniments des de /home/joan/escena-viva-esm/dades/esdeveniments.json
CATALEG
┌─────────┬───────────┬─────────────────────────────────┬────────────────────┬──────────┬───────────┬─────────┐
│ (index) │ id        │ titol                           │ sala               │ sessions │ aforament │ venudes │
├─────────┼───────────┼─────────────────────────────────┼────────────────────┼──────────┼───────────┼─────────┤
│ 0       │ 'evt-001' │ 'Concierto de Otono'            │ 'Teatro Almendra'  │ 2        │ 840       │ 276     │
│ 1       │ 'evt-002' │ 'Noche de Monologos'            │ 'Sala Boveda'      │ 3        │ 360       │ 175     │
│ 2       │ 'evt-003' │ 'Festival de Jazz de Primavera' │ 'Auditorio Ribera' │ 2        │ 1800      │ 1360    │
└─────────┴───────────┴─────────────────────────────────┴────────────────────┴──────────┴───────────┴─────────┘

Sessio ses-002-1: Noche de Monologos, 2 lliures

MEMORIA CAU DE MODULS EN ESM
┌─────────┬────────────────┬────────────────────────────┬──────────────────────┐
│ (index) │ modul          │ carregatEl                 │ dada                 │
├─────────┼────────────────┼────────────────────────────┼──────────────────────┤
│ 0       │ 'principal'    │ '2026-08-11T18:42:07.331Z' │ '3 esdeveniments'    │
│ 1       │ 'consumidor-a' │ '2026-08-11T18:42:07.331Z' │ '3 esdeveniments'    │
│ 2       │ 'consumidor-b' │ '2026-08-11T18:42:07.331Z' │ 'Noche de Monologos' │
└─────────┴────────────────┴────────────────────────────┴──────────────────────┘

Mateixa marca de temps als tres moduls? true

Respostes a les preguntes:

  • Si l'await de nivell superior trigués 3 segons, els tres mòduls que importen cataleg-dades.mjs esperarien aquests 3 segons abans d'executar la seva primera línia, i principal.mjs no arrencaria fins llavors. L'espera es propaga per tot el graf de dependències. Per això l'await de nivell superior s'ha de reservar per a inicialització imprescindible: si el catàleg es pogués carregar mandrosament o en segon pla, seria millor exportar una funció async carregarCataleg() i deixar que cadascú decideixi quan esperar.
  • Si l'await llancés una excepció no capturada, el mòdul sencer fallaria en avaluar-se. Com que els mòduls ES s'instancien abans d'executar res, l'aplicació no arrencaria gens ni mica: l'error es propagaria a tots els importadors i el procés moriria amb ERR_MODULE_NOT_FOUND o l'error original. D'aquí el try/catch de la solució: converteix una fallada fatal d'arrencada en un avís per stderr i un catàleg buit. L'aplicació arrenca, informa del problema i funciona en mode degradat, que gairebé sempre és preferible a no arrencar.

I l'última conclusió, visible a la taula: la memòria cau de mòduls també existeix en ESM. Els tres mòduls comparteixen la mateixa marca de temps perquè cataleg-dades.mjs es va avaluar un sol cop. El mecanisme és diferent per dins —un registre de mòduls per URL en lloc de require.cache— però la propietat observable és idèntica: un mòdul és un singleton al seu procés.

Conclusió

Has tancat el sistema de mòduls. Ara coneixes els mòduls ES: export amb nom i per defecte, import amb les seves variants, export * from per a les façanes, i la recomanació de preferir sempre les exportacions amb nom per autocompletat, coherència i reexportació sense sorpreses.

Sobretot, entens la diferència que ho explica tot: CommonJS és dinàmic —require és una funció que s'executa quan l'intèrpret hi arriba, amb la ruta que sigui— mentre que ESM és estàtic: les importacions són declaracions amb rutes literals, i el motor construeix el graf complet, l'instancia i només llavors l'avalua. D'aquí surten totes les conseqüències: errors d'importació detectats abans d'executar, eliminació de codi mort, importacions elevades, enllaços vius en lloc de còpies, i await de nivell superior.

Saps activar-lo de les dues maneres —"type": "module" al package.json, o les extensions .mjs i .cjs, que permeten migrar un projecte per parts— i coneixes les regles noves: les extensions són obligatòries en rutes relatives, index.js deixa de ser especial, el mode estricte està sempre actiu, i les cinc variables de l'embolcall desapareixen, substituïdes per import.meta.url amb fileURLToPath per a __dirname, createRequire per a require i pathToFileURL(process.argv[1]) per detectar el programa principal.

Domines la interoperabilitat i les seves asimetries: ESM pot importar CommonJS —sempre per defecte, i amb exportacions amb nom quan l'analitzador estàtic aconsegueix detectar-les— però CommonJS no pot fer require d'ESM, perquè require és síncron i la càrrega d'ESM és asíncrona; la via és await import(), amb el cost de tornar asíncrona la funció que ho fa.

I has traduït Escena Viva sencera. L'inventari de l'exercici va ser revelador: quatre tipus de canvi mecànics, zero canvis en la lògica. Les classes Sessio i Esdeveniment, els seus camps privats, els seus getters, el GestorDeVendes amb els seus esdeveniments: tot idèntic. El sistema de mòduls és infraestructura, i una lògica ben separada no s'assabenta que canvia.

Amb això tanquem el Mòdul 2, el més conceptual del curs. Ja no veus Node com una caixa negra: en coneixes les capes —V8, libuv, els bindings, la biblioteca estàndard—, el thread pool de quatre fils i per què bloquejar el fil principal atura el servidor sencer; pots predir línia a línia l'ordre d'execució de qualsevol programa asíncron recorrent les sis fases del bucle d'esdeveniments i les seves dues cues prioritàries; manegues les tres formes d'asincronia —callbacks error-first, promeses amb async/await, i esdeveniments amb EventEmitter— i saps quan toca cadascuna; i entens els dos sistemes de mòduls amb què conviu l'ecosistema.

Escena Viva ha deixat de ser un script amb dades incrustades. Té src/domini/ amb Sessio, Esdeveniment i GestorDeVendes, una façana a index.js, utilitats de format sense duplicar, una capa de dades a cataleg-dades.js amb una signatura ja preparada per tornar-se asíncrona, i un cataleg.js que només llegeix arguments i presenta. Els totals continuen sent els de la llavor: 3000 d'aforament, 1811 venudes, 1189 lliures.

I aquí hi ha l'última peça pendent, la que es promet des del Mòdul 1: el catàleg encara viu dins del codi. El fitxer dades/esdeveniments.json existeix des de la primera lliçó i encara no l'hem llegit ni una sola vegada des de l'aplicació. Al Mòdul 3: Sistema de Fitxers i E/S això canvia. Aprendràs a llegir i escriure fitxers amb fs, a construir rutes que funcionin en qualsevol sistema operatiu amb path, i a processar dades/vendes.csv amb streams sense carregar-lo sencer a la memòria —perquè el fitxer de vendes d'una temporada no cap al munt de V8—. Tot el que has après aquí sobre el bucle d'esdeveniments, les promeses i els EventEmitter deixa de ser teoria en aquell moment: fs.promises són promeses, els streams són EventEmitter, i la diferència entre readFile i readFileSync és exactament la diferència entre un servidor que respon i un que es congela.

Curs de Node.js: De Principiant a Avançat

Mòdul 1: Introducció a Node.js

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats