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
- La sintaxi dels mòduls ES
- La diferència essencial: estàtic contra dinàmic
- Taula comparativa completa
- Com s'activa ESM a Node
- Les extensions són obligatòries
- El que no existeix en ESM i com substituir-ho
awaitde nivell superior- Importació dinàmica:
await import() - Interoperabilitat en les dues direccions
- La versió ESM del domini d'Escena Viva
- Criteri pràctic per al curs
- 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:
// En importar, tu tries el nom: no hi ha claus.
import Sessio from './domini/sessio.mjs';
import ElQueSigui from './domini/sessio.mjs'; // Legal, i confusEs poden combinar totes dues formes:
// src/domini/esdeveniment.mjs
export default class Esdeveniment { /* ... */ }
export const ESTATS_ESDEVENIMENT = ['esborrany', 'publicat', 'finalitzat'];| 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 elmodule.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:
És una altra raó per preferir exportacions amb nom: les façanes funcionen sense sorpreses.
- 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/usar-comptador.mjs
import { compte, incrementar } from './comptador.mjs';
console.log(compte); // 0
incrementar();
console.log(compte); // 1 <-- El valor importat HA CANVIATAmb 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.
- 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.
- 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
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.jsoncomplet —nom, versió, dependències,scripts,exports— és el tema del Mòdul 5. Per ara n'hi ha prou amb saber que un fitxerpackage.jsonamb 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
- 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'; // BEError [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_moduleses continuen important pel seu nom:import express from 'express'és correcte, perquè el mateix paquet declara el seu punt d'entrada. index.jsno és especial en ESM. Cal escriure la ruta completa. Les nostres façanes passen a serimport { 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.
- 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.mjsFixa'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.
await de nivell superior
await de nivell superiorL'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.
- Importació dinàmica:
await import()
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.
- 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 funcionarLa 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
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 (senseawaitde 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 contenirawaitde 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') |
- 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:
./domini→./domini/index.js: l'extensió i l'index.jsexplícit.require.main === module→ la comparació d'URL.module.exportsal final →exporta 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.
- 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:
- É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.
- Menys fricció per aprendre.
require.main === moduleés més simple que comparar URLs,__dirnamehi és sense cerimònies, i no cal pensar en interoperabilitat mentre aprensfso Express. - 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:
- Un
package.jsonamb únicament{ "type": "module" }. src/utils/format.js,src/domini/sessio.js,src/domini/esdeveniment.js,src/domini/gestor-vendes.js,src/domini/index.js,src/cataleg-dades.jsisrc/cataleg.js.- Totes les rutes relatives amb la seva extensió, i
./domini/index.jsexplícit. require.main === modulesubstituït per la comparació ambimport.meta.url.src/informes/ocupacio.jsmigrat 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:
src/utils/format.cjs— CommonJS, ambmodule.exports = { formatarPreu, formatarData }.src/utils/llegat-dinamic.cjs— CommonJS que construeix els seus exports dinàmicament en un bucle.src/domini/sessio.mjs— ESM que importaformat.cjs(que funcioni) i intenta importar amb claus dellegat-dinamic.cjs(que falli), amb la solució aplicada.src/informe.cjs— CommonJS que necessita la classeSessiodel fitxer ESM. Demostra primer querequirefalla (captura l'error i mostra'n elcode) i després resol-ho ambawait import().- Un
README.mdamb 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:
- Carregui
dades/esdeveniments.jsonambreadFiledenode:fs/promisesiawaitde nivell superior, fent servirimport.meta.urlper construir una ruta fiable. - Tracti la fallada de lectura: si el fitxer no existeix, que registri un avís clar per
stderri continuï amb un catàleg buit en lloc d'impedir l'arrencada de l'aplicació. - Exporti
obtenirCataleg(),obtenirEsdevenimentPerId(id)iobtenirSessio(sessioId), totes tornant còpies. - Exporti també una constant
CARREGAT_ELamb 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. - Un
src/principal.mjsque faci servir tot l'anterior ambawaitde 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
// 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 EURInventari 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;
});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'
awaitde nivell superior trigués 3 segons, els tres mòduls que importencataleg-dades.mjsesperarien aquests 3 segons abans d'executar la seva primera línia, iprincipal.mjsno arrencaria fins llavors. L'espera es propaga per tot el graf de dependències. Per això l'awaitde 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'
awaitllancé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 ambERR_MODULE_NOT_FOUNDo l'error original. D'aquí eltry/catchde la solució: converteix una fallada fatal d'arrencada en un avís perstderri 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
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
