Saltar al contenido
← Volver

minimatch-fast: Cómo acelerar el matching de globs hasta 26 veces sin cambiar una línea de código

7 min de lectura
TypeScriptNPMGlobPerformanceSecurityNode.js

Minimatch es el matcher de globs más usado en el ecosistema Node.js. También tiene una vulnerabilidad de denegación de servicio desde 2022 y es lento en patrones complejos. Este paquete es un reemplazo directo que usa picomatch internamente, pasa los 355 tests originales de minimatch, y acelera el matching entre 1.3x y 26x según el patrón. Un npm install y tus globs vuelan.

Tienes un proyecto Node.js que usa minimatch para filtrar archivos. Funciona bien con patrones simples como *.js o src/**/*.ts. Pero un día añades un patrón con llaves como {src,lib}/**/*.{js,ts,jsx,tsx} y notas que el build tarda más de lo esperado. Investigas y descubres que minimatch convierte ese patrón en una expresión regular que el motor de JavaScript ejecuta con backtracking exponencial.

El problema no es solo rendimiento. Minimatch tiene CVE-2022-3517, una vulnerabilidad de denegación de servicio por expresiones regulares. Un atacante puede enviar un patrón malicioso que congela tu servidor durante minutos. La vulnerabilidad lleva años documentada pero sigue presente en millones de proyectos porque minimatch es una dependencia transitiva que nadie revisa.

Ejecutas npm ls minimatch en tu proyecto y ves que lo usan glob, fast-glob, rimraf, del, y otras diez dependencias que ni sabías que tenías. Actualizar minimatch directamente no sirve porque cada paquete trae su propia versión. Y aunque la actualizaras, el rendimiento sigue siendo el mismo.

El problema de las expresiones regulares con backtracking

Minimatch convierte patrones glob a expresiones regulares. El patrón *.js se convierte en algo como ^(?:(?!\.)(?=.)[^/]*?\.js)$. Esta expresión funciona correctamente, pero cuando el patrón tiene alternativas como {a,b,c} o rangos como {1..100}, la expresión regular resultante crece exponencialmente.

El motor de expresiones regulares de JavaScript usa backtracking para encontrar coincidencias. Cuando una rama de la expresión no coincide, el motor retrocede e intenta otra rama. Con expresiones complejas, el número de combinaciones a probar crece de forma exponencial. Un patrón que parece inocente puede generar millones de operaciones de backtracking.

La vulnerabilidad CVE-2022-3517 explota exactamente esto. Un patrón diseñado específicamente puede hacer que el motor de regex entre en un bucle de backtracking que consume CPU durante segundos o minutos. En un servidor web, esto significa que un atacante puede tumbar tu servicio enviando una petición con un patrón malicioso.

La solución: picomatch como motor interno

Picomatch es otro matcher de globs escrito por Jon Schlinkert. Usa un enfoque diferente para generar expresiones regulares que evita el backtracking catastrófico. Además, tiene límites internos en la expansión de llaves para evitar que patrones como {1..1000000} generen un millón de alternativas.

El problema de picomatch es que su API no es compatible con minimatch. Si tu código usa minimatch(path, pattern), no puedes simplemente cambiar a picomatch(pattern)(path). Tendrías que reescribir todas las llamadas, actualizar los tests, y verificar que el comportamiento sea idéntico en todos los casos extremos.

Aquí es donde entra minimatch-fast. Es una capa de compatibilidad que expone exactamente la misma API que minimatch pero usa picomatch internamente. No tienes que cambiar una línea de código. Instalas el paquete, actualizas el import, y todo funciona igual pero más rápido y sin la vulnerabilidad.

Instalación y migración

La instalación es estándar:

npm install minimatch-fast

La migración más simple es cambiar los imports:

// Antes
import { minimatch } from 'minimatch';

// Después
import { minimatch } from 'minimatch-fast';

Si no quieres tocar el código, puedes usar el aliasing de npm. Esto hace que cualquier paquete que importe minimatch reciba minimatch-fast en su lugar:

npm install minimatch@npm:minimatch-fast

Con esta opción, todas las dependencias que usan minimatch automáticamente usan minimatch-fast sin saberlo. Es útil cuando tienes dependencias transitivas que no puedes controlar.

API idéntica a minimatch

La función principal es minimatch(path, pattern, options). Recibe una ruta, un patrón glob, y opciones opcionales. Devuelve true si la ruta coincide con el patrón.

import { minimatch } from 'minimatch-fast';

minimatch('src/index.ts', '**/*.ts');           // true
minimatch('src/index.ts', '**/*.js');           // false
minimatch('.gitignore', '*', { dot: true });    // true
minimatch('SRC/Index.ts', '**/*.ts', { nocase: true }); // true

Para filtrar arrays de rutas, usa minimatch.match():

const files = ['app.js', 'app.ts', 'test.js', 'README.md'];
const jsFiles = minimatch.match(files, '*.js');
// ['app.js', 'test.js']

Si necesitas un filtro reutilizable para Array.filter():

const files = ['app.js', 'app.ts', 'test.js'];
const isTypeScript = minimatch.filter('*.ts');
const tsFiles = files.filter(isTypeScript);
// ['app.ts']

Para obtener la expresión regular compilada:

const regex = minimatch.makeRe('**/*.ts');
// Devuelve un objeto RegExp que puedes usar directamente
regex.test('src/index.ts'); // true

La expansión de llaves convierte un patrón en múltiples patrones:

minimatch.braceExpand('{src,lib}/*.js');
// ['src/*.js', 'lib/*.js']

minimatch.braceExpand('file{1..3}.txt');
// ['file1.txt', 'file2.txt', 'file3.txt']

Para escapar metacaracteres cuando el patrón viene de input de usuario:

const userInput = 'file[1].txt';
const escaped = minimatch.escape(userInput);
// 'file\\[1\\].txt'

minimatch('file[1].txt', escaped); // true (match literal)

La clase Minimatch para patrones reutilizables

Cuando vas a comparar el mismo patrón contra muchas rutas, compilar el patrón una vez es más eficiente:

import { Minimatch } from 'minimatch-fast';

const matcher = new Minimatch('**/*.{js,ts}');

const files = ['app.js', 'lib/utils.ts', 'README.md', 'src/index.tsx'];

const matches = files.filter(file => matcher.match(file));
// ['app.js', 'lib/utils.ts']

La clase expone propiedades útiles:

const m = new Minimatch('!**/*.test.js');

m.pattern;   // '!**/*.test.js'
m.negate;    // true (el patrón empieza con !)
m.comment;   // false (no es un comentario)
m.regexp;    // RegExp compilada

El método hasMagic() indica si el patrón tiene metacaracteres glob:

new Minimatch('*.js').hasMagic();      // true
new Minimatch('index.js').hasMagic();  // false

Sintaxis de patrones soportada

Los comodines básicos funcionan como esperas:

Las llaves expanden alternativas:

minimatch('src/app.js', '{src,lib}/*.js');  // true
minimatch('lib/app.js', '{src,lib}/*.js');  // true
minimatch('dist/app.js', '{src,lib}/*.js'); // false

Los rangos numéricos y alfabéticos también funcionan:

minimatch('file3.txt', 'file{1..5}.txt');  // true
minimatch('file7.txt', 'file{1..5}.txt');  // false

minimatch('section-b.md', 'section-{a..d}.md'); // true

Los patrones extglob añaden operadores de expresiones regulares:

minimatch('foo.js', '*.+(js|ts)');   // true
minimatch('foo.jsx', '*.+(js|ts)');  // false

minimatch('test.spec.js', '!(*spec*).js'); // false (contiene spec)
minimatch('app.js', '!(*spec*).js');       // true

Las clases POSIX proporcionan conjuntos de caracteres portables:

minimatch('file1.txt', 'file[[:digit:]].txt');  // true
minimatch('fileA.txt', 'file[[:alpha:]].txt');  // true
minimatch('FILE.txt', '[[:upper:]]*.txt');      // true

La negación invierte el resultado del match:

minimatch('app.js', '!*.test.js');      // true (no es un test)
minimatch('app.test.js', '!*.test.js'); // false (es un test)

Opciones de configuración

Las opciones controlan el comportamiento del matching:

dot: Por defecto, * no coincide con archivos que empiezan por punto. Activa esta opción para incluirlos.

minimatch('.gitignore', '*');              // false
minimatch('.gitignore', '*', { dot: true }); // true

nocase: Matching sin distinguir mayúsculas y minúsculas.

minimatch('README.md', '*.MD');                // false
minimatch('README.md', '*.MD', { nocase: true }); // true

matchBase: Compara el patrón solo contra el nombre del archivo, no la ruta completa.

minimatch('src/lib/utils.js', '*.js');                  // false
minimatch('src/lib/utils.js', '*.js', { matchBase: true }); // true

noglobstar: Trata ** como * (no cruza directorios).

minimatch('a/b/c.js', '**/*.js');                    // true
minimatch('a/b/c.js', '**/*.js', { noglobstar: true }); // false

nobrace: Desactiva la expansión de llaves.

minimatch('src/a.js', '{src,lib}/a.js');                 // true
minimatch('src/a.js', '{src,lib}/a.js', { nobrace: true }); // false

noext: Desactiva los patrones extglob.

minimatch('foo.js', '*.+(js|ts)');                 // true
minimatch('foo.js', '*.+(js|ts)', { noext: true }); // false

nonegate: Desactiva la negación con !.

minimatch('app.js', '!*.test.js');                   // true
minimatch('app.js', '!*.test.js', { nonegate: true }); // false

Para crear un matcher con opciones predeterminadas:

const mm = minimatch.defaults({ nocase: true, dot: true });

mm('README.MD', '*.md');  // true
mm('.env', '*');          // true

Benchmarks: entre 1.3x y 26x más rápido

Los benchmarks se ejecutaron en Node.js 22 comparando minimatch 10.0.1 contra minimatch-fast 1.0.0. Los resultados varían según el tipo de patrón:

Patrones simples (*.js, **/*.ts): 1.35x más rápido. La mejora es modesta porque estos patrones ya son eficientes en minimatch.

Patrones con negación (!*.test.js): 1.50x más rápido.

Patrones con llaves ({src,lib}/**/*.{js,ts}): 6.5x más rápido. Aquí es donde picomatch brilla porque evita la explosión combinatoria de alternativas.

Patrones con llaves complejas ({a,b,c}/{d,e,f}/**/*.{js,ts,jsx,tsx}): 26.6x más rápido. Minimatch genera una expresión regular gigante mientras que picomatch mantiene el rendimiento lineal.

Clase Minimatch precompilada: 1.16x más rápido en uso repetido del mismo patrón.

En términos prácticos, si tu build tarda 10 segundos procesando globs con patrones complejos, puede bajar a menos de 1 segundo. En patrones simples la mejora es menor pero nunca es más lento que minimatch.

Seguridad: protección contra ReDoS

La vulnerabilidad CVE-2022-3517 permite ataques de denegación de servicio mediante expresiones regulares. Un atacante envía un patrón diseñado para causar backtracking catastrófico y tu servidor se congela.

minimatch-fast mitiga esto de dos formas:

Motor de regex seguro: Picomatch genera expresiones regulares que no sufren backtracking exponencial. Los patrones que congelan minimatch se ejecutan instantáneamente en minimatch-fast.

Límites en expansión de llaves: La expansión de rangos como {1..1000} tiene límites para evitar que un patrón genere millones de alternativas. Minimatch original se congela con {1..100000}. minimatch-fast lo procesa en milisegundos con un límite razonable.

// Esto congela minimatch durante segundos o minutos
// minimatch('file.js', '{1..100000}.js');

// minimatch-fast lo maneja sin problemas
minimatch('file.js', '{1..100000}.js'); // false, procesado en ms

Si tu aplicación recibe patrones de usuarios externos, minimatch-fast es la opción segura.

Compatibilidad verificada con 355 tests

El paquete incluye 355 tests que verifican compatibilidad exacta con minimatch:

Todos los tests de minimatch original pasan. Si tu código funciona con minimatch, funciona con minimatch-fast sin cambios.

Soporte TypeScript completo

El paquete incluye definiciones de tipos completas:

import {
  minimatch,
  Minimatch,
  type MinimatchOptions,
} from 'minimatch-fast';

const options: MinimatchOptions = {
  nocase: true,
  dot: true,
};

const matcher: Minimatch = new Minimatch('**/*.ts', options);
const result: boolean = matcher.match('src/index.ts');

Los tipos son idénticos a los de @types/minimatch, así que el autocompletado y verificación de tipos funcionan igual.

Cuándo usar minimatch-fast

Migración desde minimatch: Si ya usas minimatch y quieres mejor rendimiento o eliminar la vulnerabilidad CVE-2022-3517, minimatch-fast es un cambio de una línea.

Proyectos nuevos: Si vas a usar glob matching, empieza directamente con minimatch-fast en lugar de minimatch.

Patrones complejos: Si usas patrones con muchas llaves o rangos, la mejora de rendimiento es dramática.

Aplicaciones que reciben patrones externos: Si los usuarios pueden enviar patrones glob, minimatch-fast protege contra ataques ReDoS.

Dependencias transitivas: Usa el aliasing de npm para que todas tus dependencias usen minimatch-fast automáticamente.

El código está en GitHub

El paquete está publicado en NPM como minimatch-fast. El código fuente está en github.com/686f6c61/minimatch-fast.

La licencia es MIT, igual que minimatch original. Puedes usarlo en proyectos comerciales sin restricciones.

Si encuentras incompatibilidades con minimatch o tienes sugerencias, abre un issue en GitHub. El objetivo es mantener compatibilidad al 100% mientras mejoramos rendimiento y seguridad.