jgm
v0.2.0
Published
JS Glob Matcher.
Readme
🎲 JS Glob Matcher | JGM
Легкая библиотека для сопоставления путей файлов с GLOB-паттернами.
npm i jgmВ отличие от традиционных библиотек, которые просто возвращают массив строк, jgm предоставляет полный контроль над процессом обхода через связку CompiledMatcher и RelPath.
📌 Ключевые особенности
- Производительность: Паттерны компилируются один раз в оптимизированное представление, что делает сопоставление на каждом шаге обхода чрезвычайно быстрым.
- Управляемый обход: Методы
.can()(можно ли войти в каталог?) и .test()(соответствует ли файл?) дают полный контроль над процессом. Объектная модельRelPath- это иммутабельный и кеширующий объект пути. - Гибкость и расширяемость: Все ключевые классы (
Glob,RelPath) можно наследовать для создания собственной, уникальной логики. - Гранулярная настройка: Управляйте чувствительностью к регистру и поддержкой Unicode для каждого паттерна индивидуально.
- Независимость от платформы: Ядро библиотеки написано на чистом
TSи не привязано кNode.js. Вы можете написать свой "драйвер" для обхода любой древовидной структуры.
🚀 Quick start
Настройте правила и получите управляемый обход дерева.
import { join } from 'node:path'
import { readdir, lstat } from 'node:fs/promises'
import {
type TMatcherOptions,
Matcher,
Glob,
RelPath,
CompiledMatcher,
} from 'jgm'
async function walkAsync (root: string, matcher: Matcher): Promise<string[]> {
const files: string[] = []
const rec = async (rel: RelPath) => {
const entries = await readdir(join(root, rel.toString()), {
encoding: 'utf8',
recursive: false,
withFileTypes: true
})
for (const item of entries) {
// Расширяем корневой путь
const relPath = rel.extends(item.name)
if (item.isDirectory()) {
// Для каталогов вызывается can() - Определяет возможность входа в каталог.
if (matcher.can(relPath)) {
await rec(relPath)
}
}
else if (item.isFile()) {
// Тестирование пути к файлу - Определяем подходит ли путь паттерну.
if (matcher.test(relPath)) {
files.push(relPath.toString())
}
}
else {
console.warn(`unsupported file of symlink path: "${relPath.toString()}"`)
}
}
}
// Корневой путь создается статическим методом и не имеет имени.
await rec(RelPath.root())
return files
}
const options: TMatcherOptions = {
include: ['src/**/*?.ts', 'package.json', 'lic*.md', 'readme.md'],
exclude: ['**/*.test.ts', '**/node_modules/**'],
maxDepth: 4 // максимально допустимая глубина обхода
}
const matcher = new CompiledMatcher(options)
const files = await walkAsync(targetDir, matcher)
expect(files).toEqual(expect.arrayContaining(['src/glob.ts', ... ]))🔮 API
Опциональные параметры
- include
(string | Glob)[]Массив паттернов или экземпляровGlobдля включения. - exclude
(string | Glob)[]Массив паттернов для исключения. Правила исключения имеют приоритет над включением. - maxDepth
numberМаксимальная глубина обхода. По умолчанию64. - noIgnoreCase
booleanЕслиtrue, все паттерны будут чувствительны к регистру(отключает флаг RegExpi). По умолчаниюfalse. КаждыйGlobможно переопределить индивидуально. - experimentalUnicode
booleanВключает строгий Unicode-режим (флагuдля RegExp). По умолчаниюfalse.
Основные классы
Ниже представлены только основные методы классов.
- Glob - Скомпилированный Glob-паттерн с методами подбора пути.
- .test(RelPath, isDir) - Соответствует ли финальный путь (обычно файл) заданным правилам
include/exclude. ПараметрisDirимеет эффект только для паттернов с явно заданным конечным слешем:'src/utils/', в противном случае игнорируется. - .can(RelPath) - Следует ли входить в каталог. Это ключевой метод для оптимизации, который отсекает ненужные ветки дерева.
- .test(RelPath, isDir) - Соответствует ли финальный путь (обычно файл) заданным правилам
- RelPath - Путь относительно корневой директории обхода.
- static root() - Создает корневой путь.
- extends(filename) - Создает новый дочерний путь. Каждый сегмент пути привязан к родителю. Сегменты кешируют результаты сопоставлений.
- Matcher - Основной интерфейс с набором скомпилированных Glob-паттернов для подбора пути(абстрактный класс).
- .test(RelPath) - Делегирует вызовы
Glob.test(). - .can(RelPath) - Делегирует вызовы
Glob.can().
- .test(RelPath) - Делегирует вызовы
- CompiledMatcher - Реализация
Matcherс набором скомпилированных Glob-паттернов для подбора пути. - NoopMatcher - Реализация
Matcherбез паттернов, которая может применяться как заглушка.
⚒️ Правила GLOB-паттернов
jgm поддерживает ограниченный, но самый растространенный набор правил.
* ** ? {js,ts} /Поддерживаемые символы:
'*'(звездочка) - Соответствует любой последовательности(или пустоте) символов, кроме разделителя пути ('/').'**'(две звездочки) - Соответствует любой последовательности символов, включая разделители пути ('/'). Используется для рекурсивного поиска. Для эффективности'**'лучше всего использовать в сочетании с разделителями:'**/','/**/'или'/**'.'?'(знак вопроса) - Соответствует одному любому символу.'{,}'(группа) - Соответствует любому из перечисленных через запятую паттернов. Вложенные группы не поддерживаются, но паттерны могут иметь другие специальные символы.'<...>/'(завершающий слеш) - Соответствует только директориям.'<...>/<...>'(правый слеш) - Разделитель пути.'/literal/'- Любая комбинация символов между разделителями, кроме одних звездочек, конвертируется кRegExp.'\\{<...>\\}'(обратный слеш) - Экранирует специальные символы, позволяя использовать их в именах файлов. Последовательность обратных слешей сливается в один и экранирует следующий за ним символ. Обратные слеши в именах файлов не поддерживаются.
Правила:
'/<...>'- Начальные слеши игнорируются и обрезаются. Пример:'/src' -> 'src'- одно и то же. Все паттерны привязаны к корню каталога. Для получения вложенных файлов нужно использовать'**/<...>'или привязываться к конкретному каталогу'src/**/<...>'.'<...>/'- Завершающий слеш однозначно определяет паттерн соответствующий только директории.'*/*/*.txt'- Одна звездочка между разделителями определяет один уровень каталога.'**/*/**/*/**'- Любая комбинация с одинарными звездочками и слешами, определяем минимальную глубину уровней равную количеству одинарных звездочек.'foo /\\ / bar '- Любая бесполезная последовательность, приводящая к пробельным символам вокруг разделителей, игнорируется. Результатом будет очищенный набор сегментов['foo', 'bar'].'foo.{bar, }'- Пробельные символы в скобках не очищаются, что может привести к имени файла с конечным пробелом'foo. '. Такие имена бесполезны и их следует избегать.
🎯 Практические примеры:
{
// Включает корневой package.json и рекурсивно обходит
// src на любую глубину
"include": ["package.json", "src/**"],
// Исключает все файлы и каталоги с ведущей точкой
// и каталоги с именем node_modules
"exclude": ["**/.*", "**/node_modules/"]
}Файлы с именами(без расширения) 'build' пройдут, но на вложенном каталоге обход остановится:
{
"include": ["**"],
"exclude": ["**/build/"]
}Соберет файлы верхнего уровня, но не войдет ни в один каталог:
{
"include": ["*?.{js,ts}"]
}Соберет все файлы, кроме первого уровня:
{
"include": ["*/**"] // равнозначно "**/*"
}Экранирование специальных символов. Пропустит файл/каталог с именем стандартного UUID со скобками:
{
"include": ["**/\\{????????-????-?????????-????????????\\}"]
}Наглядное представление нормализованного паттерна:
new Glob('**/srs/*/*/?/**/*/*/*.{js,ts}')
[
{ kind: 2, depth: 0, re: null },
{ kind: 4, depth: 1, re: /^srs$/i },
{ kind: 1, depth: 2, re: null },
{ kind: 4, depth: 1, re: /^.$/i },
{ kind: 3, depth: 2, re: null },
{ kind: 4, depth: 1, re: /.*\.(js|ts)$/i }
]