@uzum-tech/design-tokens-resolver 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,642 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <img width="144px" src="https://ik.imagekit.io/jbalancer/uzum-logo.svg?updatedAt=1692012347116" />
3
+ </p>
2
4
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
5
+ # @uzum-tech/design-tokens-resolver
6
+
7
+ > Забирает опубликованные дизайн-токены из сервиса дизайн-токенов — по номеру версии
8
+ > или по каналу раскатки, — сверяет целостность и кладёт на диск.
9
+ > Команда токенов двигает канал, продуктовые проекты ничего не
10
+ > подтверждают руками.
11
+
12
+ ---
13
+
14
+ ## Содержание
15
+
16
+ - [Зачем](#зачем)
17
+ - [Установка](#установка)
18
+ - [Быстрый старт](#быстрый-старт)
19
+ - [Конфигурация](#конфигурация)
20
+ - [Стенды](#стенды)
21
+ - [Объект или async-геттер](#объект-или-async-геттер)
22
+ - [Доступ: ключ или Basic](#доступ-ключ-или-basic)
23
+ - [Все поля конфига](#все-поля-конфига)
24
+ - [Командная строка](#командная-строка)
25
+ - [Версия или канал](#версия-или-канал)
26
+ - [Форма выгрузки](#форма-выгрузки)
27
+ - [API инстанса](#api-инстанса)
28
+ - [Как работает проверка целостности](#как-работает-проверка-целостности)
29
+ - [Повторы при сбоях](#повторы-при-сбоях)
30
+ - [Лок-файл](#лок-файл)
31
+ - [Ошибки](#ошибки)
32
+ - [Рецепты](#рецепты)
33
+ - [FAQ](#faq)
34
+
35
+ ---
36
+
37
+ ## Зачем
38
+
39
+ Дизайн-токены живут в Figma, а нужны в коде — и в вебе, и на мобилках.
40
+ Между ними стоит движок: он хранит неизменяемые релизы и
41
+ указатели-каналы (`latest`, `next`). Резолвер — единственное, что нужно
42
+ продуктовой команде, чтобы получить нужную версию в свой репозиторий.
43
+
44
+ Что внутри:
45
+
46
+ - **выбор версии** — по номеру или по каналу раскатки, на который сейчас
47
+ указывает команда дизайн-системы;
48
+ - **проверка целостности** — `sha256` канонического тела сверяется с
49
+ `contentHash` релиза до того, как что-то попадёт на диск;
50
+ - **стенды** — потребитель называет `local`, `development` или
51
+ `production`, адреса знает пакет;
52
+ - **нарезки** — одна коллекция, форма `flat` или `dtcg`, разворачивание
53
+ алиасов до литеральных значений;
54
+ - **лок-файл** — паспорт сборки: версия, хеш, адрес движка, кто и когда
55
+ опубликовал;
56
+ - **повторы** — временные отказы сети не роняют пайплайн.
57
+
58
+ Публикация токенов и их выкатка в продукт разведены намеренно. Резолвер
59
+ ничего не публикует и не двигает — только читает.
60
+
61
+ ---
62
+
63
+ ## Установка
64
+
65
+ ```bash
66
+ pnpm add -D @uzum-tech/design-tokens-resolver
67
+ ```
68
+
69
+ Требуется **Node.js >= 20.18.1**. Зависимостей у пакета нет.
70
+
71
+ Разово, без установки:
72
+
73
+ ```bash
74
+ npx @uzum-tech/design-tokens-resolver --env development --channel latest
75
+ ```
76
+
77
+ Пакет собран в двух форматах, поэтому работает и в CommonJS-проектах,
78
+ и в ESM:
79
+
80
+ ```ts
81
+ // ESM
82
+ import DesignTokensResolver from '@uzum-tech/design-tokens-resolver';
83
+
84
+ // CommonJS
85
+ const DesignTokensResolver = require('@uzum-tech/design-tokens-resolver').default;
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Быстрый старт
91
+
92
+ Кладём в корень проекта `design-tokens.config.mjs`:
93
+
94
+ ```js
95
+ export default () => ({
96
+ env: process.env.DESIGN_TOKENS_ENV ?? 'production',
97
+ apiKey: process.env.DESIGN_TOKENS_API_KEY,
98
+
99
+ outputPath: './src/tokens',
100
+ collection: 'base-b2b',
101
+ channel: 'latest'
102
+ });
103
+ ```
104
+
105
+ Добавляем скрипт и зовём его перед сборкой:
106
+
107
+ ```json
108
+ {
109
+ "scripts": {
110
+ "tokens": "design-tokens-resolver --resolve",
111
+ "prebuild": "pnpm tokens"
112
+ }
113
+ }
114
+ ```
115
+
116
+ ```
117
+ $ pnpm tokens
118
+ Движок https://tokens.example.com/api/v1
119
+ Версия 1.0.0, хеш d56a34ce7331061c1ea3433b6ed5a7669727797b6b9d98dea950229df989db38
120
+ Токены: src/tokens/tokens.json (12958 байт)
121
+ Лок: src/tokens/tokens.lock.json
122
+ ```
123
+
124
+ Адрес движка печатается первой строкой, до любых запросов — по логу
125
+ сборки всегда видно, откуда приехали токены.
126
+
127
+ Оба файла стоит коммитить: `tokens.json` — то, что попадёт в сборку,
128
+ `tokens.lock.json` — чем это подтверждается.
129
+
130
+ ---
131
+
132
+ ## Конфигурация
133
+
134
+ Резолвер ищет конфиг вверх по дереву от текущего каталога. Порядок
135
+ поиска:
136
+
137
+ ```
138
+ design-tokens.config.mjs
139
+ design-tokens.config.js
140
+ design-tokens.config.cjs
141
+ design-tokens.config.json
142
+ ```
143
+
144
+ Исполняемый конфиг предпочтительнее: в нём доступны переменные
145
+ окружения, поэтому ключи не приходится дублировать в настройках
146
+ пайплайна.
147
+
148
+ ### Стенды
149
+
150
+ Адресов движка в конфиге проекта нет: потребитель называет стенд, адреса
151
+ знает пакет.
152
+
153
+ | Стенд | Назначение |
154
+ |---|---|
155
+ | `local` | локальный стенд разработчика |
156
+ | `development` | тестовый стенд |
157
+ | `production` | боевой стенд |
158
+
159
+ По умолчанию `production` — продуктовому проекту не нужно задавать
160
+ ничего. Инфраструктура переезжает — чинится в одном месте, в пакете, а не
161
+ в конфиге каждого репозитория.
162
+
163
+ Опечатка в имени ловится до обращения к сети:
164
+
165
+ ```
166
+ $ design-tokens-resolver --env stage
167
+ CONFIG_INVALID: Unknown environment "stage". Known: local, development, production
168
+ ```
169
+
170
+ Если движок поднят не по адресу стенда — например, локально на другом
171
+ порту, — есть `baseUrl`. Он перекрывает выбор целиком.
172
+
173
+ ### Объект или async-геттер
174
+
175
+ Экспортом может быть и сам объект, и функция — синхронная либо
176
+ асинхронная:
177
+
178
+ ```js
179
+ // объект
180
+ export default {
181
+ env: 'production',
182
+ channel: 'latest'
183
+ };
184
+
185
+ // async-геттер: ключ можно достать из хранилища секретов
186
+ export default async () => ({
187
+ env: 'production',
188
+ apiKey: await readSecret('design-tokens-api-key'),
189
+ channel: 'latest'
190
+ });
191
+ ```
192
+
193
+ Возвращать нужно объект по умолчанию (`export default`) — без него
194
+ резолвер бросит `CONFIG_INVALID`.
195
+
196
+ ### Доступ: ключ или Basic
197
+
198
+ Поддерживаются оба способа, ключ имеет приоритет:
199
+
200
+ | Способ | Откуда берётся |
201
+ |---|---|
202
+ | API-ключ | `--api-key`, `DESIGN_TOKENS_API_KEY`, либо `apiKey` в конфиге |
203
+ | Basic | `--username`/`--password`, `DESIGN_TOKENS_USERNAME`/`DESIGN_TOKENS_PASSWORD`, либо `auth` в конфиге |
204
+
205
+ Ключ уходит заголовком `x-api-key`, Basic — заголовком `authorization`.
206
+ Если не задано ничего, резолвер ходит без заголовков — движок при этом
207
+ ответит `401`, если выгрузка релизов на нём закрыта. Сам резолвер доступ
208
+ не требует: ключ заводится и хранится на стороне потребителя.
209
+
210
+ На `401` в подсказке к ошибке перечисляется, что именно нужно задать.
211
+
212
+ ### Все поля конфига
213
+
214
+ Все поля опциональны:
215
+
216
+ | Поле | Тип | По умолчанию | Описание |
217
+ |---|---|---|---|
218
+ | `env` | `ConfigTypes.Env` | `'production'` | Стенд: `local`, `development`, `production` |
219
+ | `baseUrl` | `string` | — | Адрес движка в обход стендов |
220
+ | `apiKey` | `string` | — | Уходит заголовком `x-api-key` |
221
+ | `auth` | `ConfigTypes.Auth` | — | `{ username, password }` для Basic |
222
+ | `channel` | `string` | — | Канал раскатки: `latest` или `next` |
223
+ | `collection` | `string` | — | Отдать только одну коллекцию |
224
+ | `outputPath` | `string` | `process.cwd()` | Каталог для файлов |
225
+ | `fileName` | `string` | `'tokens.json'` | Имя файла с токенами |
226
+ | `requestTimeoutInMs` | `number` | `30000` | Таймаут одного запроса |
227
+ | `retryCount` | `number` | `2` | Сколько раз повторить временный отказ |
228
+ | `retryDelayInMs` | `number` | `500` | Базовая пауза между попытками |
229
+
230
+ Версия в конфиг намеренно не кладётся: `--version` — разовый флаг для
231
+ хотфикса или расследования. Иначе проект тихо отстанет от канала.
232
+
233
+ ---
234
+
235
+ ## Командная строка
236
+
237
+ | Флаг | Что делает |
238
+ |---|---|
239
+ | `--env <стенд>` | `local`, `development` или `production`; либо `DESIGN_TOKENS_ENV` |
240
+ | `--base-url <url>` | адрес движка в обход стендов; либо `DESIGN_TOKENS_BASE_URL` |
241
+ | `--version <semver>` | конкретная версия релиза |
242
+ | `--channel <slug>` | взять версию, на которую сейчас указывает канал |
243
+ | `--collection <slug>` | только одна коллекция |
244
+ | `--format <flat\|dtcg>` | форма выгрузки, по умолчанию `flat` |
245
+ | `--resolve` | развернуть алиасы до литеральных значений |
246
+ | `--output-path <путь>` | куда сохранить |
247
+ | `--file-name <имя>` | имя файла, по умолчанию `tokens.json` |
248
+ | `--no-lock` | не писать `tokens.lock.json` |
249
+ | `--api-key <ключ>` | либо `DESIGN_TOKENS_API_KEY` |
250
+ | `--username <логин>` | либо `DESIGN_TOKENS_USERNAME` |
251
+ | `--password <пароль>` | либо `DESIGN_TOKENS_PASSWORD` |
252
+
253
+ Приоритет источников: **флаг → переменная окружения → конфиг проекта →
254
+ умолчание**.
255
+
256
+ ---
257
+
258
+ ## Версия или канал
259
+
260
+ Версию можно задать двумя способами, и они взаимоисключающие по смыслу:
261
+
262
+ ```bash
263
+ design-tokens-resolver --channel latest # то, на что сейчас указывает канал
264
+ design-tokens-resolver --version 1.0.0 # конкретный релиз
265
+ ```
266
+
267
+ Если заданы оба, побеждает `--version`. Если не задан ни один —
268
+ `CONFIG_INVALID`, до обращения к сети.
269
+
270
+ Каналов два: `latest` — стабильный, `next` — обкатать релиз до того, как
271
+ он станет стабильным. Канал может существовать, но ещё ни на что не
272
+ указывать — тогда `CHANNEL_EMPTY`.
273
+
274
+ Продуктовому проекту нужен канал, а не версия: тогда выкатка новых
275
+ токенов — решение команды дизайн-системы, а не задача каждого
276
+ репозитория.
277
+
278
+ ---
279
+
280
+ ## Форма выгрузки
281
+
282
+ `flat` — всегда документ одной и той же формы, независимо от
283
+ `--collection` и `--resolve`:
284
+
285
+ ```json
286
+ {
287
+ "schemaVersion": 1,
288
+ "collections": [
289
+ { "slug": "base-b2b", "layout": "SINGLE_MODE", "modes": [ { "slug": "default", "isDefault": true } ] }
290
+ ],
291
+ "tokens": [
292
+ { "collection": "base-b2b", "mode": "default", "path": "color.blue.100", "$type": "color", "$value": "#eef2fc" },
293
+ { "collection": "product", "mode": "b2b-dark", "path": "accent.brand", "$type": "color", "$value": "{base-b2b.color.violet.500}" }
294
+ ]
295
+ }
296
+ ```
297
+
298
+ `--collection` сужает и `tokens`, и `collections`; `--resolve` заменяет
299
+ ссылки вида `{коллекция.путь}` на литеральные значения. Ключи верхнего
300
+ уровня при этом не меняются, поэтому кодогенератор проекта можно писать
301
+ один раз и не переучивать под набор флагов.
302
+
303
+ `dtcg` — другая форма по определению: дерево групп. С `--collection` это
304
+ дерево одной коллекции, без него — дерево деревьев, по одному на
305
+ коллекцию.
306
+
307
+ Кодогенерации почти всегда нужен `--resolve`: без него алиасы приходят
308
+ ссылками, и разворачивать их придётся самому.
309
+
310
+ ---
311
+
312
+ ## API инстанса
313
+
314
+ ```ts
315
+ import DesignTokensResolver, { ConfigTypes } from '@uzum-tech/design-tokens-resolver';
316
+
317
+ const resolver = new DesignTokensResolver({
318
+ env: ConfigTypes.Env.PRODUCTION,
319
+ apiKey: process.env.DESIGN_TOKENS_API_KEY
320
+ });
321
+ ```
322
+
323
+ Конструктор резолвит конфиг сразу, поэтому неверный стенд или неполный
324
+ логин-пароль бросят `CONFIG_INVALID` на месте, а не на первом запросе.
325
+
326
+ #### `resolve(request): Promise<ResolveTypes.Result>`
327
+
328
+ Главный метод: выбирает версию, проверяет целостность, пишет токены и
329
+ лок на диск.
330
+
331
+ ```ts
332
+ const result = await resolver.resolve({
333
+ channel: 'latest',
334
+ collection: 'base-b2b',
335
+ resolve: true,
336
+ outputPath: './src/tokens'
337
+ });
338
+
339
+ result.release.version; // '1.0.0'
340
+ result.tokensPath; // 'src/tokens/tokens.json'
341
+ result.lockPath; // 'src/tokens/tokens.lock.json' либо null
342
+ result.bytesWritten; // 12958
343
+ result.source.baseUrl; // 'https://tokens.example.com/api/v1'
344
+ ```
345
+
346
+ Пути возвращаются в том же виде, в каком задан `outputPath`: относительный
347
+ путь остаётся относительным. Каталог создаётся, если его нет.
348
+
349
+ `lock: false` отключает запись лока — тогда `lockPath` будет `null`.
350
+
351
+ #### `fetchVerified(target): Promise<ResolveTypes.Verified>`
352
+
353
+ То же самое, но без записи на диск: отдаёт проверенное тело и метаданные
354
+ релиза. Удобно, если нужно собрать что-то своё.
355
+
356
+ ```ts
357
+ const { release, body } = await resolver.fetchVerified({
358
+ version: '1.0.0',
359
+ format: ConfigTypes.Format.DTCG,
360
+ collection: 'base-b2b'
361
+ });
362
+ ```
363
+
364
+ #### `resolveVersion(target): Promise<string>`
365
+
366
+ Превращает канал в номер версии. Если версия уже задана — возвращает её
367
+ как есть, без сетевых запросов.
368
+
369
+ ```ts
370
+ const version = await resolver.resolveVersion({ channel: 'latest' });
371
+ // '1.0.0'
372
+ ```
373
+
374
+ Бросает `CHANNEL_EMPTY`, если канал существует, но ещё ни на что не
375
+ указывает, и `RELEASE_NOT_FOUND`, если такого канала нет.
376
+
377
+ #### `getSource(): ResolveTypes.Source`
378
+
379
+ Адрес движка, с которым работает экземпляр. Сетевых запросов не делает —
380
+ это то, что CLI печатает первой строкой.
381
+
382
+ ---
383
+
384
+ ## Как работает проверка целостности
385
+
386
+ Перед записью резолвер всегда запрашивает **каноническое**
387
+ представление релиза — без фильтров, `format=flat`, без резолва — и
388
+ сверяет его `sha256` с `contentHash` из метаданных релиза. Не сошлось —
389
+ `INTEGRITY_MISMATCH`, и на диск не пишется ничего.
390
+
391
+ Порядок такой:
392
+
393
+ 1. канал → версия, если версия не задана явно;
394
+ 2. метаданные релиза, включая `contentHash`;
395
+ 3. каноническое тело и сверка хеша;
396
+ 4. нарезка отдельным запросом, если заданы `--collection`, `--resolve`
397
+ или `--format dtcg`.
398
+
399
+ Четвёртый шаг — цена за третий: хеш считается от канонической формы, а
400
+ любая нарезка меняет байты. Один лишний GET за то, что подменённые в базе
401
+ байты не доедут до сборки незамеченными.
402
+
403
+ Хеш зависит от содержимого, а не от номера версии. Это важно, потому что
404
+ dev и prod — независимые движки со своими таблицами релизов, и версия
405
+ `1.0.0` на них может содержать разное.
406
+
407
+ ---
408
+
409
+ ## Повторы при сбоях
410
+
411
+ Сетевой сбой и «пятисотка» — не повод ронять сборку, поэтому запросы
412
+ повторяются: три попытки, паузы 500 мс и 1 секунда.
413
+
414
+ | Ответ | Поведение |
415
+ |---|---|
416
+ | обрыв связи, таймаут | повтор |
417
+ | `429`, любой `5xx` | повтор |
418
+ | `401`, `403` | сразу ошибка — доступ сам не починится |
419
+ | `404` | сразу ошибка — версии или канала нет |
420
+
421
+ Отдельный запрос не может висеть дольше таймаута — по умолчанию 30
422
+ секунд:
423
+
424
+ ```js
425
+ export default () => ({
426
+ env: 'production',
427
+ requestTimeoutInMs: 30000,
428
+ retryCount: 2,
429
+ retryDelayInMs: 500
430
+ });
431
+ ```
432
+
433
+ `retryCount: 0` выключает повторы совсем. В худшем случае команда займёт
434
+ `(retryCount + 1) × requestTimeoutInMs` плюс паузы — это стоит держать в
435
+ голове, выставляя таймаут джобы в пайплайне.
436
+
437
+ ---
438
+
439
+ ## Лок-файл
440
+
441
+ Рядом с токенами пишется `tokens.lock.json`:
442
+
443
+ ```json
444
+ {
445
+ "version": "1.0.0",
446
+ "contentHash": "d56a34ce7331061c1ea3433b6ed5a7669727797b6b9d98dea950229df989db38",
447
+ "baseUrl": "https://tokens.example.com/api/v1",
448
+ "byteSize": 222111,
449
+ "publishedAt": "2026-09-04T08:15:31.237Z",
450
+ "publishedBy": "designer",
451
+ "sourceLabel": "figma-2026-08"
452
+ }
453
+ ```
454
+
455
+ Времени скачивания в нём нет — только свойства релиза. Поэтому повторный
456
+ запуск на той же версии даёт побайтово тот же файл, а дифф в merge
457
+ request показывает смену версии, а не шум.
458
+
459
+ Лок отвечает на вопрос «откуда это приехало» месяц спустя: какая версия,
460
+ с какого движка, кто и когда её опубликовал. В CI его стоит класть в
461
+ артефакты рядом с бандлом.
462
+
463
+ ---
464
+
465
+ ## Ошибки
466
+
467
+ Все ошибки пакета — экземпляры `ResolverError` с полем `code`:
468
+
469
+ ```ts
470
+ import { ErrorTypes, ResolverError } from '@uzum-tech/design-tokens-resolver';
471
+
472
+ try {
473
+ await resolver.resolve({ channel: 'latest', outputPath: './src/tokens' });
474
+ } catch (error) {
475
+ if (ResolverError.is(error, ErrorTypes.Code.REQUEST_FAILED)) {
476
+ error.statusCode; // 401
477
+ error.body; // тело ответа движка
478
+ }
479
+ }
480
+ ```
481
+
482
+ | Код | Когда возникает |
483
+ |---|---|
484
+ | `CONFIG_INVALID` | Неизвестный стенд, неполный логин-пароль, не заданы ни версия ни канал, конфиг без `export default` |
485
+ | `REQUEST_FAILED` | Движок недоступен или ответил статусом >= 400 |
486
+ | `RELEASE_NOT_FOUND` | Нет такой версии или такого канала |
487
+ | `CHANNEL_EMPTY` | Канал есть, но пока ни на что не указывает |
488
+ | `INTEGRITY_MISMATCH` | `sha256` тела не совпал с `contentHash` релиза |
489
+ | `WRITE_FAILED` | Не удалось записать файл |
490
+
491
+ `ResolverError.is(error)` без второго аргумента проверяет только тип.
492
+ Оба варианта сужают тип для TypeScript, так что после проверки
493
+ `error.statusCode` доступен без приведения.
494
+
495
+ В CLI ошибка печатается как `КОД: сообщение`, процесс завершается с
496
+ кодом `1`. На `401` добавляется подсказка, какие переменные окружения
497
+ нужно задать.
498
+
499
+ ---
500
+
501
+ ## Рецепты
502
+
503
+ ### Вебовый проект
504
+
505
+ ```json
506
+ {
507
+ "scripts": {
508
+ "tokens": "design-tokens-resolver --resolve",
509
+ "tokens:css": "node scripts/tokens-to-css.mjs",
510
+ "predev": "pnpm tokens && pnpm tokens:css",
511
+ "prebuild": "pnpm tokens && pnpm tokens:css"
512
+ }
513
+ }
514
+ ```
515
+
516
+ Разовые сценарии поверх тех же скриптов:
517
+
518
+ ```bash
519
+ pnpm tokens --version 1.4.0 # хотфикс на конкретной версии
520
+ pnpm tokens --channel next # посмотреть, что идёт следующим
521
+ DESIGN_TOKENS_ENV=development pnpm tokens
522
+ ```
523
+
524
+ ### GitLab CI
525
+
526
+ ```yaml
527
+ tokens:
528
+ variables:
529
+ DESIGN_TOKENS_ENV: production
530
+ script:
531
+ - pnpm i --frozen-lockfile
532
+ - pnpm tokens
533
+ - git diff --exit-code ./src/tokens || echo "токены обновились"
534
+ artifacts:
535
+ paths:
536
+ - src/tokens/tokens.lock.json
537
+ ```
538
+
539
+ Ключ — в переменных CI, masked и protected. Стенд задаётся одной
540
+ переменной, адрес подставляет пакет.
541
+
542
+ ### Две коллекции в одном проекте
543
+
544
+ Лок всегда называется `tokens.lock.json`, поэтому двум коллекциям нужны
545
+ разные каталоги:
546
+
547
+ ```bash
548
+ design-tokens-resolver --collection base-b2b --output-path ./src/tokens/base
549
+ design-tokens-resolver --collection product --output-path ./src/tokens/product
550
+ ```
551
+
552
+ ### Свой кодогенератор поверх тела
553
+
554
+ ```ts
555
+ import DesignTokensResolver from '@uzum-tech/design-tokens-resolver';
556
+
557
+ const resolver = new DesignTokensResolver({ env: 'production' });
558
+
559
+ const { release, body } = await resolver.fetchVerified({
560
+ channel: 'latest',
561
+ collection: 'base-b2b',
562
+ resolve: true
563
+ });
564
+
565
+ const document = JSON.parse(body);
566
+
567
+ for (const token of document.tokens) {
568
+ // token.path, token.$type, token.$value, token.mode
569
+ }
570
+
571
+ console.log(`собрано из релиза ${release.version}`);
572
+ ```
573
+
574
+ ### Проверка, что токены в репозитории не отстали
575
+
576
+ ```bash
577
+ design-tokens-resolver --channel latest --no-lock --output-path /tmp/tokens
578
+ diff -q /tmp/tokens/tokens.json ./src/tokens/tokens.json
579
+ ```
580
+
581
+ ---
582
+
583
+ ## FAQ
584
+
585
+ **Инстанс создавать один на процесс или на запрос?**
586
+ Как удобно. Состояния между вызовами резолвер не хранит: кеша нет, всё
587
+ живёт в файлах на диске.
588
+
589
+ **Что коммитить: `tokens.json`, лок или оба?**
590
+ Оба, если токены участвуют в сборке продукта. Тогда сборка
591
+ воспроизводима без сети, а дифф показывает, что именно изменилось.
592
+ Вариант «только лок, а токены — артефакт сборки» тоже рабочий, но тогда
593
+ сборка без движка невозможна.
594
+
595
+ **Почему в конфиге канал, а не версия?**
596
+ Чтобы решение о выкатке принимала команда дизайн-системы. С версией в
597
+ конфиге проект тихо отстанет от `latest`, и никто этого не заметит.
598
+
599
+ **Токены с dev попали в prod-сборку — как понять?**
600
+ По локу: в нём лежит `baseUrl`, с которого приехал релиз. Стенд задаёт
601
+ `DESIGN_TOKENS_ENV` или поле `env` в конфиге — искать надо там.
602
+
603
+ **Сборка упала с `INTEGRITY_MISMATCH` — что делать?**
604
+ Не обходить. Это значит, что байты релиза в базе не совпадают с
605
+ заявленным `contentHash`: либо релиз правили в обход движка, либо ответ
606
+ подменили по дороге. Идти к команде дизайн-системы.
607
+
608
+ **Движок лежит, а собраться надо.**
609
+ Файлы от прошлого запуска лежат на диске и закоммичены — соберитесь
610
+ напрямую, минуя `prebuild`. `--version` не поможет: номер версии тоже
611
+ берётся с движка.
612
+
613
+ **Нужен только `tokens.json`, без генерации.**
614
+ Тогда `prebuild: design-tokens-resolver` — и всё; кодогенератор
615
+ появляется по потребности проекта.
616
+
617
+ **Как посмотреть, что лежит в релизе, ничего не записывая?**
618
+ `fetchVerified()` отдаёт проверенное тело строкой. Из CLI — `--no-lock`
619
+ и `--output-path` во временный каталог.
620
+
621
+ **Можно ли использовать слои по отдельности?**
622
+ Да, из корня пакета экспортируются `DesignClient`, `HttpClient`,
623
+ `ConfigResolver`, `ConfigFileResolver`, `DocumentMapper`, `LockMapper`,
624
+ `ResolveMapper`, `HashUtils`, `UrlUtils`, `FileUtils`, `ResolveUtils` и
625
+ `ArgsUtils` — если фасад чем-то не подходит, соберите свой из тех же
626
+ деталей.
627
+
628
+ ---
629
+
630
+ ## Разработка
631
+
632
+ ```bash
633
+ pnpm i
634
+ pnpm typecheck
635
+ pnpm lint
636
+ pnpm test
637
+ pnpm build
638
+ ```
639
+
640
+ `pnpm i` ставит husky-хуки. На коммит выполняется `pnpm precommit` —
641
+ `typecheck`, `lint-staged` и тесты параллельно; сообщение коммита
642
+ получает префикс из имени ветки.