svelte-tiers
v0.1.2
Published
Modular routing system for SvelteKit. Builds the routes directory from multiple sources (tiers) with priorities.
Maintainers
Readme
svelte-tiers
En | Ru
Модульная маршрутизация для SvelteKit. Разделяйте маршруты по нескольким исходным папкам («слоям», tiers) и объединяйте их в единую папку маршрутов на этапе сборки — с точным контролем над тем, какой слой чем «владеет».
Оглавление
- Что это делает
- Установка
- Быстрый старт
- Как это работает
- Конфигурация
- Приоритет и конфликты
- Как работает
level - Фильтр
match - Разработка и hot reload
- Сгенерированная папка
- Полный справочник конфигурации
- Рецепты
- FAQ
- Устранение неполадок
Что это делает
Обычный SvelteKit ожидает все маршруты в одной папке (src/routes). Это
становится неудобным, когда большое приложение хочется разбить на
самодостаточные модули (например, blog, shop, admin), каждый из
которых несёт свои маршруты.
svelte-tiers позволяет объявить несколько исходных папок маршрутов и
объединяет их в одну папку, которую затем использует SvelteKit. Вы сохраняете
модульную структуру файлов; SvelteKit видит единое плоское дерево маршрутов.
Основные возможности:
- Несколько источников маршрутов («слоёв») с упорядочиванием по приоритету.
- Маска
{module}, которая автоматически разворачивается на каждую папку модуля. - Параметр
level, управляющий тем, какой частью своего дерева владеет слой и что могут дополнять слои с более низким приоритетом. - Опциональная фильтрация по имени файла через
match. - Прозрачный passthrough: без настроенных слоёв работает ровно как обычный
sveltekit(). - Hot reload в дев-режиме: изменения в любом слое пересобирают результат и перезагружают страницу.
Установка
npm install -D svelte-tiers
# или
pnpm add -D svelte-tiers
# или
bun add -d svelte-tiers@sveltejs/kit и vite — это peer-зависимости (они уже есть в проекте
SvelteKit).
Быстрый старт
Замените плагин sveltekit() в vite.config.js на svelteTiers():
// vite.config.js
import { defineConfig } from 'vite';
import { svelteTiers } from 'svelte-tiers';
export default defineConfig({
plugins: [await svelteTiers()],
});Затем объявите слои в svelte.config.js:
// svelte.config.js
import adapter from '@sveltejs/adapter-auto';
export default {
kit: {
adapter: adapter(),
tiers: [
'src/routes',
'src/modules/{module}/routes',
],
},
};Теперь такая структура:
src/
routes/
+layout.svelte
+page.svelte
modules/
blog/
routes/
blog/+page.svelte
shop/
routes/
shop/+page.svelte…объединяется в единое дерево маршрутов, которое обслуживает SvelteKit.
svelteTiers()— асинхронная функция, всегда используйтеawait(или вызывайте её внутри асинхронной функции конфигурации).
Как это работает
- При старте
svelteTiers()читает конфигурациюkit(либо переданный вами объект, либо изsvelte.config.js). - Если
tiersзадан (непустой), он:- разрешает каждый слой, разворачивая маски
{module}; - проходит по слоям по порядку и вычисляет объединённую карту файлов
согласно правилам приоритета и
level; - записывает результат в папку слияния (по умолчанию
.tiers/routes); - указывает
files.routesSvelteKit на эту папку; - устанавливает dev-watcher Vite, пересобирающий результат при изменениях.
- разрешает каждый слой, разворачивая маски
- Затем оборачивает и возвращает настоящий плагин (или плагины)
sveltekit().
Папка слияния специально находится вне .svelte-kit/ — Vite игнорирует
.svelte-kit/ в своём файловом наблюдателе, что сломало бы hot reload
объединённых маршрутов.
Конфигурация
Где хранится конфигурация
Есть два варианта:
A. Конфигурация в svelte.config.js (рекомендуется) — вызывайте
svelteTiers() без аргументов; он загрузит поле kit из svelte.config.js:
// vite.config.js
plugins: [await svelteTiers()]// svelte.config.js
export default {
kit: {
/* обычные опции kit */
tiers: [ /* ... */ ],
},
};B. Конфигурация прямо в вызове — передайте объект в форме kit:
// vite.config.js
plugins: [
await svelteTiers({
adapter: adapter(),
tiers: ['src/routes', 'src/modules/{module}/routes'],
}),
]Объект принимает те же поля, что и kit в svelte.config.js, плюс tiers.
Режим passthrough (прозрачная обёртка)
Важно. Если
tiersне задан или задан как пустой массив (tiers: []), плагин не делает ничего особенного — он превращается в прозрачную обёртку надsveltekit().
В режиме passthrough он:
- не переопределяет
files.routes; - не создаёт папку
.tiers/routes; - не запускает watcher слияния;
- просто передаёт все опции
kitвsveltekit().
Поэтому оба варианта ниже ведут себя ровно как прямой вызов sveltekit():
// вообще без ключа tiers
export default { kit: { /* ... */ } };
// либо явный пустой массив
export default { kit: { tiers: [], /* ... */ } };Режим слияния активируется только тогда, когда tiers содержит хотя бы один
элемент. Это позволяет безопасно поставить svelteTiers() как обёртку по
умолчанию и включить модульную маршрутизацию позже.
Опция tiers
tiers — это упорядоченный массив. Каждый элемент — это либо строка
(сокращённая запись), либо объект:
tiers: [
// сокращённая запись → { src, level: Infinity, match: null }
'src/routes',
// полная форма (объект)
{
src: 'src/modules/{module}/routes',
level: Infinity,
match: null,
},
]| Поле | Тип | По умолчанию | Значение |
| ------- | -------------------------- | ------------ | ------------------------------------------------------------------- |
| src | string | — | Исходная папка. Может содержать маску {module} (разворачивается по папкам). |
| level | number | Infinity | Насколько глубоко слой «захватывает» дерево (см. level). |
| match | string \| RegExp \| null | null | Опциональный фильтр по имени файла. null = все файлы. |
Порядок = приоритет. Более ранние слои выигрывают конфликты (см. ниже).
Маска {module}
Литеральный токен {module} внутри src разворачивается на каждую
непосредственную вложенную папку, найденную в этой позиции. Например:
src: 'src/modules/{module}/routes'с папками:
src/modules/blog/routes
src/modules/shop/routes
src/modules/admin/routesразворачивается в три эффективных слоя — по одному на модуль — все они наследуют
одинаковые level и match. Модули обрабатываются вместе как часть этого
единого элемента слоя, после всех слоёв, объявленных до него.
Слой без {module} указывает ровно на одну папку.
Приоритет и конфликты
Слои обрабатываются в порядке объявления. Когда два слоя дают один и тот же целевой путь, более ранний слой выигрывает, а более поздний пропускается.
Есть два вида «выигрыша»:
- file-claim (захват файла) — точный путь файла уже занят слоем с более высоким приоритетом; дубликат из нижнего слоя отбрасывается.
- dir-claim (захват папки) — целое поддерево папки зарезервировано слоем с
более высоким приоритетом; нижние слои не могут добавить туда ничего.
Dir-claim возникает из-за
level(см. следующий раздел).
Как работает level
level — сердце плагина. Он управляет тем, какая часть дерева слоя считается
единым самодостаточным блоком, а какую могут расширять слои с более низким
приоритетом.
Глубина отсчитывается с нуля, относительно корня src слоя:
- файлы прямо в корне имеют глубину
0; - файлы/папки на уровень ниже — глубину
1; - и так далее.
Правило
- Файлы выше границы уровня объединяются по отдельности — слой с более низким приоритетом всё ещё может добавить соседние файлы по этим путям, которых пока нет.
- Каждая папка на границе уровня копируется целиком (со всем поддеревом) и захватывается — ни один слой с более низким приоритетом не может добавить внутрь неё что-либо.
«Берётся целиком и захватывается» означает: всё содержимое папки берётся из этого слоя, и она становится dir-claim, блокирующим нижние слои.
Шпаргалка по level
| level | Поведение |
| ---------- | ----------------------------------------------------------------------------- |
| 0 | Только файлы прямо в корне. В папки вообще не заходим. |
| 1 | Файлы корня + каждая папка верхнего уровня берётся целиком и захватывается. |
| 2 | Файлы на глубине 0–1 + каждая папка на глубине 2 берётся целиком/захватывается. |
| n | Файлы на глубине 0 … n-1 + каждая папка на глубине n берётся целиком/захватывается. |
| Infinity | Всё, рекурсивно. Ни одна папка не захватывается — чистое пофайловое слияние. |
Примеры
Во всех примерах ниже используются два слоя:
tiers: [
/* слой 1, меняется в каждом примере */,
'src/modules/{module}/routes', // слой 2 (более низкий приоритет, level: Infinity)
]и такие исходники:
src/routes/
+layout.svelte
+page.svelte
blog/
+page.svelte
src/modules/blog/routes/
blog/
+page.svelte ← конфликтует с blog/+page.svelte слоя 1
[id]/
+page.svelte ← новый
about/
+page.svelte ← новыйПример A — level: Infinity (чистое слияние)
{ src: 'src/routes', level: Infinity } // или просто 'src/routes'Ничего не захватывается как блок. Слой 1 выигрывает точные конфликты; слой 2
заполняет все остальные пути — даже внутри blog/.
Результат в .tiers/routes:
+layout.svelte (слой 1)
+page.svelte (слой 1)
blog/+page.svelte (слой 1 — выигрывает конфликт)
blog/[id]/+page.svelte (слой 2 — добавлен, конфликта нет)
about/+page.svelte (слой 2 — добавлен, конфликта нет)Пример B — level: 1 (захват папок верхнего уровня)
{ src: 'src/routes', level: 1 }+layout.svelte,+page.svelte(глубина 0) — объединяются обычным образом.blog/(папка на глубине 1) — берётся целиком и захватывается. Слой 2 не может добавить внутрьblog/ничего.
Результат в .tiers/routes:
+layout.svelte (слой 1)
+page.svelte (слой 1)
blog/+page.svelte (слой 1 — весь blog/ захвачен)
about/+page.svelte (слой 2 — about/ НЕ захвачен слоем 1)Отличия от примера A:
blog/[id]/+page.svelteиз слоя 2 отброшен —blog/захвачен как папка.about/+page.svelteвсё же добавлен — у слоя 1 нет папкиabout/.
Смысл level: 1: «какой раздел (папку верхнего уровня) я определяю — тем
владею целиком; но разделы, которые я не определяю, могут добавлять модули».
Пример C — level: 0 (только файлы корня)
{ src: 'src/routes', level: 0 }Слой 1 добавляет только файлы прямо в корне и никогда не заходит в папки:
+layout.svelte (слой 1 — файл корня)
+page.svelte (слой 1 — файл корня)
blog/+page.svelte (слой 2 — слой 1 не заходил в blog/)
blog/[id]/+page.svelte (слой 2)
about/+page.svelte (слой 2)Здесь слой 1 предоставляет лишь «оболочку» приложения (+layout, корневой
+page), а всеми разделами владеют модули.
Пример D — level: 2 (захват глубже)
{ src: 'src/routes', level: 2 }С исходником:
src/routes/
+layout.svelte глубина 0
blog/
+page.svelte глубина 1
[id]/ глубина 2 ← папка НА границе
+page.svelteПоведение:
- файлы на глубине 0–1 (
+layout.svelte,blog/+page.svelte) — объединяются по отдельности; нижние слои могут добавлять отсутствующие соседние файлы. blog/[id]/(папка на глубине 2) — берётся целиком и захватывается. Слой 2 не может добавить ничего подblog/[id]/, но может добавить, например,blog/tags/+page.svelte(эта папка не захвачена).
Как выбрать level
| Цель | Значение |
| -------------------------------------------------------------------------- | ------------ |
| Базовое приложение владеет всем; модули лишь заполняют недостающие файлы | Infinity |
| Базовое приложение владеет целыми разделами, которые определяет; модули добавляют новые разделы | 1 |
| Базовое приложение владеет только оболочкой; модули владеют всеми разделами | 0 |
| Точечный захват на конкретной глубине | 2, 3, … |
Фильтр match
match ограничивает, какие файлы добавляет слой. Папки никогда не
фильтруются по match (фильтруются только файлы). Принимает:
- точное имя файла — строка (
'+page.svelte'); RegExp(/\.svelte$/);- строку в форме
/pattern/flags('/\\+(page|layout)\\.svelte/').
tiers: [
// добавлять только файлы +page.svelte
{ src: 'src/routes', match: '+page.svelte' },
// регулярка: только *.svelte
{ src: 'src/modules/{module}/routes', match: /\.svelte$/ },
// строковая форма регулярки (обратите внимание на экранирование)
{ src: 'src/extra', match: '/\\+(page|layout)\\.svelte/' },
]match: null (по умолчанию) означает, что проходят все файлы.
Разработка и hot reload
В дев-режиме плагин следит за всеми исходными папками слоёв. Когда вы:
- меняете файл → результат пересобирается, а затронутые модули инвалидируются в графе модулей Vite;
- добавляете файл или папку → они попадают в слияние, и страница перезагружается;
- удаляете файл или папку → они удаляются из слияния, и страница перезагружается.
Небольшой debounce объединяет серии изменений; полная перезагрузка страницы отправляется после того, как манифест маршрутов SvelteKit успел пересобраться — чтобы новые маршруты не отдавали 404 без перезапуска dev-сервера.
Сгенерированная папка
По умолчанию результат слияния пишется в:
.tiers/routesОна генерируется — не редактируйте вручную; ваши правки будут перезаписаны.
Добавьте её в
.gitignore:.svelte-kit/ .tiers/Она специально находится вне
.svelte-kit/(Vite игнорирует.svelte-kit/в своём наблюдателе, что сломало бы hot reload объединённых маршрутов).
Местоположение можно переопределить через files.routes (см. ниже).
Полный справочник конфигурации
Все опции передаются внутри объекта kit (в svelte.config.js или прямо в
svelteTiers()).
{
kit: {
// ── все стандартные опции `kit` SvelteKit поддерживаются и пробрасываются ──
adapter: adapter(),
alias: { /* ... */ },
// ...
// ── специфичные для svelte-tiers ──
tiers: [
// сокращённая запись (строка)
'src/routes',
// полная форма (объект)
{
src: 'src/modules/{module}/routes', // обязательно
level: Infinity, // по умолчанию: Infinity
match: null, // по умолчанию: null
},
],
// Опционально: переопределить местоположение папки слияния.
// По умолчанию: '.tiers/routes'. Используется только когда `tiers` непустой.
files: {
routes: '.tiers/routes',
},
},
}Поля слоя
| Поле | Тип | Обязательно | По умолчанию | Описание |
| ------- | -------------------------- | ----------- | ------------ | -------------------------------------------------------------- |
| src | string | да | — | Исходная папка; может включать маску {module}. |
| level | number | нет | Infinity | Глубина захвата (с нуля). См. level. |
| match | string \| RegExp \| null | нет | null | Фильтр по имени файла. null = все файлы. |
Флаги поведения плагина (вычисляемые, не задаются вами)
| Условие | Результат |
| -------------------------------- | ------------------------------------------------------------------- |
| tiers отсутствует или [] | Режим passthrough: обычный sveltekit(), files.routes не трогается. |
| tiers непустой | Режим слияния: files.routes указывает на папку слияния, watcher включён. |
| files.routes задан + слияние | Местоположение папки слияния переопределяется на этот путь. |
Рецепты
Модульное приложение с базовой оболочкой
Базовое приложение владеет layout и разделами верхнего уровня, которые определяет; модули добавляют собственные разделы:
tiers: [
{ src: 'src/routes', level: 1 },
'src/modules/{module}/routes',
]Тонкое ядро, маршрутизация от модулей
Ядро предоставляет только +layout и главную страницу; всё остальное — из
модулей:
tiers: [
{ src: 'src/routes', level: 0 },
'src/modules/{module}/routes',
]Переопределения, которые могут заполнять пробелы
Слой overrides, выигрывающий точные конфликты, но пропускающий всё остальное:
tiers: [
'src/overrides', // высший приоритет, Infinity
'src/routes',
'src/modules/{module}/routes',
]Инъекция только layout-файлов
Слой, добавляющий лишь layout-файлы, оставляя страницы из других источников:
tiers: [
{ src: 'src/layouts', match: /\+layout(\.\w+)?\.(svelte|js|ts)$/ },
'src/routes',
'src/modules/{module}/routes',
]FAQ
Нужно ли создавать файлы в .tiers/routes?
Нет. Она генерируется. Редактируйте исходники слоёв (src/routes,
src/modules/...).
Что если два модуля определяют один и тот же маршрут?
Модули внутри одного слоя {module} имеют одинаковый приоритет. Первый
разрешённый выигрывает конфликт точного пути; возможно, будет выведено
предупреждение. Реструктурируйте или используйте слой с более высоким
приоритетом для устранения неоднозначности.
Можно ли использовать без модульной маршрутизации?
Да — это режим passthrough. Не задавайте
tiers, и это будет просто sveltekit().
Работает ли с +layout, +page, +server, ...+error, .js/.ts файлами?
Да — он объединяет файлы по пути независимо от типа. level/match применяются
единообразно.
Влияет ли это на продакшн-сборки? Слияние формируется на этапе конфигурации/старта и при изменениях файлов, поэтому и dev, и build используют одну и ту же сгенерированную папку.
Можно ли смешивать слои с разными level в одной конфигурации?
Да. Каждый слой обрабатывается со своим собственным level. Типичный паттерн —
базовый слой с level: 0 или 1 и слой модулей с Infinity.
Что произойдёт, если папка модуля пустая?
Пустой модуль (или папка, не содержащая подходящих под match файлов) просто
ничего не добавляет в результат слияния. Ошибки не возникает.
Можно ли иметь несколько масок {module} в разных слоях?
Да. Каждый слой с {module} разворачивается независимо. Например, можно иметь
src/modules/{module}/routes и src/plugins/{module}/routes как отдельные слои.
Устранение неполадок
Новые маршруты отдают 404 в dev.
Слияние пересобирается и запускает перезагрузку, но манифесту SvelteKit нужен
момент. Если совершенно новый маршрут не появился — сохраните ещё раз или
перезапустите dev. Убедитесь, что папка слияния — это .tiers/routes (вне
.svelte-kit/) — маршруты внутри .svelte-kit/ не отслеживаются Vite и не будут
надёжно перезагружаться на лету.
Изменения не подхватываются.
Проверьте, что изменённый файл лежит внутри одного из корней tiers[].src и не
исключён фильтром match этого слоя.
Маршрут модуля молча отсутствует.
Скорее всего, он захвачен как папка (dir-claim) слоем с более высоким
приоритетом и низким level. Повысьте level этого слоя или переместите
конфликтующую папку. См. Как работает level.
Всё маршрутизируется так, будто слоёв нет.
Вероятно, вы в режиме passthrough — проверьте, что tiers присутствует и
непустой в той конфигурации, которую фактически загружает плагин.
svelteTiers is not a function / неожиданный промис.
svelteTiers() асинхронна — её нужно await-ить в vite.config.js.
Ошибка invalid tier при старте.
Элемент слоя должен быть либо строкой, либо объектом с полем src. Проверьте,
что все элементы tiers корректны (в частности, у объектной формы задан src).
Ошибка invalid match.
Значение match должно быть null, строкой (точное имя или форма
/pattern/flags) или RegExp. Другие типы вызывают ошибку.
Конфликт двух модулей — какой выиграл, непредсказуемо.
Порядок разворачивания {module} зависит от порядка чтения подпапок. Если вам
нужен детерминированный приоритет между конкретными модулями — вынесите
приоритетный модуль в отдельный слой выше по списку tiers.
Краткая шпаргалка
// svelte.config.js
export default {
kit: {
adapter: adapter(),
// Нет tiers / tiers: [] → passthrough (как обычный sveltekit())
// Непустой tiers → режим слияния в .tiers/routes
tiers: [
// 1) строка = { src, level: Infinity, match: null }
'src/routes',
// 2) объект = точный контроль
{
src: 'src/modules/{module}/routes',
level: Infinity, // Infinity | 0 | 1 | 2 | n
match: null, // null | '+page.svelte' | /\.svelte$/ | '/.../flags'
},
],
},
};| Хочу… | level слоя 1 |
| ---------------------------------------------- | ------------ |
| Базовое приложение владеет всем, модули заполняют пробелы | Infinity |
| Базовое владеет своими разделами, модули добавляют новые | 1 |
| Базовое даёт только оболочку, модули владеют разделами | 0 |
| Точечный захват на глубине N | N |
Помните:
- Более ранний слой = более высокий приоритет.
levelопределяет, какие папки слой «захватывает» целиком.- Папку
.tiers/routesне редактируют — она генерируется. - Без
tiersплагин = прозрачная обёртка надsveltekit().
