[{"data":1,"prerenderedAt":1654},["ShallowReactive",2],{"page-ru-\u002Fposts\u002Fessential-frontend-automations":3},{"id":4,"title":5,"body":6,"date":1641,"description":1642,"duration":1643,"extension":1644,"meta":1645,"navigation":817,"path":1646,"robots":1647,"seo":1648,"sitemap":1649,"stem":1650,"tags":1651,"toc":817,"__hash__":1653},"content_ru\u002Fposts\u002Fessential-frontend-automations.md","Необходимые фронтенд автоматизации",{"type":7,"value":8,"toc":1626},"minimark",[9,17,24,29,33,36,40,43,59,63,66,71,82,88,102,116,119,146,150,163,166,170,178,238,252,263,266,286,289,292,295,299,302,311,333,345,357,361,364,377,400,406,410,413,416,425,429,444,453,462,465,786,899,906,1081,1084,1088,1091,1094,1109,1112,1352,1484,1491,1583,1612,1615,1619,1622],[10,11,13],"h1",{"id":12},"",[14,15],"binding",{"value":16},"title",[18,19],"posts-date-duration",{":duration":20,":date":21,"className":22},"duration","date",[23],"-mt-6",[25,26,28],"h2",{"id":27},"введение","Введение",[30,31,32],"p",{},"За годы работы с фронтендом я перепробовал кучу инструментов автоматизации — часть оказалась временным хайпом, а часть настоящими находками, которые я использую и по сей день.",[30,34,35],{},"Сегодня расскажу про свои любимые автоматизации, которые считаю полезными (а некоторые — прямо необходимыми). Все их я испытывал на проде, и кое-что из этого отличается от типичного пайплайна.",[25,37,39],{"id":38},"дисклеймер","Дисклеймер",[30,41,42],{},"Намеренно не трогаю LLM-автоматизации по нескольким причинам:",[44,45,46,50,53,56],"ul",{},[47,48,49],"li",{},"Их уже столько, что хватит еще на 1–2 статьи",[47,51,52],{},"У меня пока мало опыта с LLM-автоматизациями именно на проде",[47,54,55],{},"Они постоянно появляются, меняются и эволюционируют",[47,57,58],{},"Текущие автоматизации можно использовать внутри LLM-агентов — они хорошо дополняют друг друга",[25,60,62],{"id":61},"автоматизации","Автоматизации",[30,64,65],{},"Статья идет от более известных инструментов к менее раскрученным. Можно пользоваться оглавлением слева и сразу прыгать к интересующей области.",[67,68,70],"h3",{"id":69},"линтинг-и-форматирование-eslint-eslint-stylistic","Линтинг и форматирование = ESLint + ESLint Stylistic",[30,72,73,74,81],{},"Самая очевидная автоматизация — конечно же ",[75,76,80],"a",{"href":77,"rel":78},"https:\u002F\u002Feslint.org\u002F",[79],"nofollow","ESLint",".",[83,84,85],"blockquote",{},[30,86,87],{},"А где Prettier?",[30,89,90,91,96,97,81],{},"Да, ",[75,92,95],{"href":93,"rel":94},"https:\u002F\u002Fprettier.io\u002F",[79],"Prettier"," здесь нет. На мой взгляд, лучше использовать ",[75,98,101],{"href":99,"rel":100},"https:\u002F\u002Feslint.style\u002F",[79],"ESLint Stylistic",[30,103,104,105,110,111,115],{},"Подробнее об этом можно почитать в ",[75,106,109],{"href":107,"rel":108},"https:\u002F\u002Fantfu.me\u002Fposts\u002Fwhy-not-prettier",[79],"Why I don't use Prettier",", но мой главный аргумент такой: ",[112,113,114],"strong",{},"если что-то из стиля Prettier тебе не нравится, ты будешь с ним бороться и городить костыли",". Часть поведения нельзя тонко настроить, а что-то вообще нельзя переопределить. ESLint дает куда больше гибкости там, где она нужна, и при этом не жертвует полезными фичами.",[30,117,118],{},"Если вы еще не пробовали такой подход — очень советую.",[30,120,121,122,127,128,133,134,139,140,145],{},"Также стоит внимательно наблюдать за ",[75,123,126],{"href":124,"rel":125},"https:\u002F\u002Foxc.rs\u002Fdocs\u002Fguide\u002Fusage\u002Flinter",[79],"Oxlint"," и ",[75,129,132],{"href":130,"rel":131},"https:\u002F\u002Foxc.rs\u002Fdocs\u002Fguide\u002Fusage\u002Fformatter",[79],"Oxfmt",". На момент написания статьи Oxlint еще не поддерживает многие правила ESLint (",[75,135,138],{"href":136,"rel":137},"https:\u002F\u002Fgithub.com\u002Foxc-project\u002Foxc\u002Fissues\u002F481",[79],"общие",", ",[75,141,144],{"href":142,"rel":143},"https:\u002F\u002Fgithub.com\u002Foxc-project\u002Foxc\u002Fissues\u002F15761",[79],"vue","), но на момент прочтения все может измениться.",[67,147,149],{"id":148},"тестирование-playwright-ночные-смоки","Тестирование = Playwright + ночные смоки",[30,151,152,153,156,157,162],{},"В тестировании автоматизаций полно, но одну недооценивают особенно сильно: ",[112,154,155],{},"ночные E2E-смоки",". Смоук-тесты — это небольшой набор E2E-кейсов, который покрывает самые используемые сценарии продукта. Если хотите, чтобы продукт был максимально стабильным — запускайте их каждую ночь. Для E2E сейчас лучше всего, на мой взгляд, подходит ",[75,158,161],{"href":159,"rel":160},"https:\u002F\u002Fplaywright.dev\u002F",[79],"Playwright"," — сильная поддержка от Microsoft, читаемее Cypress и есть все основные интеграции.",[30,164,165],{},"Самый крутой и недооцененный плюс — ловить регрессии в типовых сценариях до следующего рабочего дня, часто раньше, чем кто-то успеет заметить. Если никто не заметил, можно считать, что этого и не было. Чистая история ночных прогонов еще и хороший аргумент, когда речь заходит о надежности или повышении.",[67,167,169],{"id":168},"качество-кода-knip-husky","Качество кода = Knip + Husky",[30,171,172,177],{},[75,173,176],{"href":174,"rel":175},"https:\u002F\u002Fknip.dev\u002F",[79],"Knip"," помогает найти мертвый код. Минус Knip в том, что при auto-imports нужна дополнительная настройка — такие файлы по умолчанию выглядят неиспользуемыми. Минимальный фикс — пометить их как entry points или игнорировать:",[179,180,184],"pre",{"className":181,"code":182,"language":183,"meta":12,"style":12},"language-json shiki shiki-themes github-light github-dark","{\n  \"entry\": [\"src\u002Fmain.ts\", \"src\u002Fcomponents\u002F**\u002F*.vue\"],\n  \"ignore\": [\"src\u002Fauto-imports.d.ts\"]\n}\n","json",[185,186,187,196,218,232],"code",{"__ignoreMap":12},[188,189,192],"span",{"class":190,"line":191},"line",1,[188,193,195],{"class":194},"sVt8B","{\n",[188,197,199,203,206,210,212,215],{"class":190,"line":198},2,[188,200,202],{"class":201},"sj4cs","  \"entry\"",[188,204,205],{"class":194},": [",[188,207,209],{"class":208},"sZZnC","\"src\u002Fmain.ts\"",[188,211,139],{"class":194},[188,213,214],{"class":208},"\"src\u002Fcomponents\u002F**\u002F*.vue\"",[188,216,217],{"class":194},"],\n",[188,219,221,224,226,229],{"class":190,"line":220},3,[188,222,223],{"class":201},"  \"ignore\"",[188,225,205],{"class":194},[188,227,228],{"class":208},"\"src\u002Fauto-imports.d.ts\"",[188,230,231],{"class":194},"]\n",[188,233,235],{"class":190,"line":234},4,[188,236,237],{"class":194},"}\n",[30,239,240,245,246,251],{},[75,241,244],{"href":242,"rel":243},"https:\u002F\u002Ftypicode.github.io\u002Fhusky\u002F",[79],"Husky"," позволяет запускать команды на git-хуках — чаще всего на pre-commit. Типичные сценарии: запуск ",[75,247,250],{"href":248,"rel":249},"https:\u002F\u002Fgithub.com\u002Flint-staged\u002Flint-staged",[79],"lint-staged",", чтобы линтить только staged-файлы, или проверка формата commit-сообщений.",[30,253,254,255,258,259,262],{},"Но мое мнение: ",[112,256,257],{},"я бы не использовал Husky",", или как минимум ",[112,260,261],{},"не блокировал бы им коммиты",". Те же проверки можно реализовать в CI, и там есть контролируемый способ их обойти, когда это реально нужно.",[30,264,265],{},"Блокирующие pre-commit хуки в приватных командах почти никогда не стоят того:",[267,268,269,280,283],"ol",{},[47,270,271,272,275,276,279],{},"Эти правила легко отключить через ",[185,273,274],{},"HUSKY=0"," (или ",[185,277,278],{},"--no-verify",") без какого-либо следа или отчета.",[47,281,282],{},"Если обход все же перекрыт (кастомные git-обертки, жесткие политики хуков и т.д.) — удачи с хотфиксами, которые надо выкатить ASAP. Попробуйте объяснить менеджеру, что фикс не вышел из-за ошибки ESLint. Плюс становятся недоступны временные коммиты (лично я ими часто пользуюсь).",[47,284,285],{},"Даже это не гарантирует отключения правил — пользователь может просто удалить их или сам Husky у себя на машине.",[30,287,288],{},"В итоге поздравляю: вы сильно ухудшили DX, получив наполовину «защищенный» пайплайн.",[30,290,291],{},"Считать локальные хуки Husky настоящей границей безопасности — примерно как хранить JWT-секрет в браузере. На собеседовании присутствие блокирующих Husky-хуков в команде я бы серьезно считал желто-красным флагом.",[30,293,294],{},"Пожалуйста, используйте Husky только если понимаете, что делаете.",[67,296,298],{"id":297},"версии-зависимостей-taze-renovate","Версии зависимостей = taze + Renovate",[30,300,301],{},"Управлять зависимостями относительно просто, но делать это нужно регулярно. Если автоматизировать хотя бы часть процесса — в долгую выигрыш будет заметным.",[30,303,304,305,310],{},"Можно проверять каждый пакет вручную, а можно взять ",[75,306,309],{"href":307,"rel":308},"https:\u002F\u002Fgithub.com\u002Fantfu-collective\u002Ftaze",[79],"taze",". Taze показывает, у каких пакетов есть обновления, раскладывает их по semver (major, minor, patch) и умеет применять их автоматически:",[179,312,316],{"className":313,"code":314,"language":315,"meta":12,"style":12},"language-bash shiki shiki-themes github-light github-dark","pnpm dlx taze -w\n","bash",[185,317,318],{"__ignoreMap":12},[188,319,320,324,327,330],{"class":190,"line":191},[188,321,323],{"class":322},"sScJk","pnpm",[188,325,326],{"class":208}," dlx",[188,328,329],{"class":208}," taze",[188,331,332],{"class":201}," -w\n",[30,334,335,336,341,342,81],{},"Как альтернатива — бот ",[75,337,340],{"href":338,"rel":339},"https:\u002F\u002Fdocs.renovatebot.com\u002F",[79],"Renovate"," (или любой похожий). Он сам создает PR и апдейтит ",[185,343,344],{},"package.json",[30,346,347,348,350,351,356],{},"Честно говоря, мне ближе ",[112,349,309],{},": он работает с ",[75,352,355],{"href":353,"rel":354},"https:\u002F\u002Fpnpm.io\u002Fcatalogs",[79],"pnpm catalogs",", и мне нравится после каждого обновления пакета локально проверять сборку — просто чтобы быть уверенным. Но оба варианта, на мой взгляд, вполне ок для прода.",[67,358,360],{"id":359},"управление-монорепой-turborepo-pnpm","Управление монорепой = Turborepo + pnpm",[30,362,363],{},"У монорепозиториев темная история — долгое время это был набор костылей, чтобы заставить все репозитории жить вместе. Сейчас монорепы заметно проще, но все равно сложнее обычного репозитория.",[30,365,366,367,127,372,376],{},"Я создавал и поддерживал много монорепозиториев и считаю ",[75,368,371],{"href":369,"rel":370},"https:\u002F\u002Fturbo.build\u002F",[79],"Turborepo",[75,373,323],{"href":374,"rel":375},"https:\u002F\u002Fpnpm.io\u002F",[79]," необходимыми для этого.",[30,378,379,380,383,384,139,389,139,394,399],{},"Честно думаю, что ",[112,381,382],{},"pnpm стоит использовать вместо npm по умолчанию"," в любом репозитории — он просто быстрее и экономнее по диску. Несколько источников: ",[75,385,388],{"href":386,"rel":387},"https:\u002F\u002Fdev.to\u002Fpockit_tools\u002Fpnpm-vs-npm-vs-yarn-vs-bun-the-2026-package-manager-showdown-51dc",[79],"pnpm vs npm vs yarn vs Bun",[75,390,393],{"href":391,"rel":392},"https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=ZIKDJBrk56k",[79],"I Finally Changed Package Managers",[75,395,398],{"href":396,"rel":397},"https:\u002F\u002Fantfu.me\u002Fposts\u002Fcategorize-deps",[79],"Categorize Your Dependencies",". А для монореп pnpm уже давно стал стандартом, тогда как npm только догоняет (например, workspaces появились только в 2020, а catalogs до сих пор нет).",[30,401,402,405],{},[75,403,371],{"href":369,"rel":404},[79]," — настраиваемая система сборки с кучей фич: кэш сборки (чтобы не пересобирать пакеты воркспейса каждый раз, когда пересобираете фасадное приложение) и центральный оркестратор команд (чтобы, например, прогнать lint по всем воркспейсам).",[67,407,409],{"id":408},"версии-пакетов-changesets","Версии пакетов = Changesets",[30,411,412],{},"Управлять версиями пакетов сложнее, чем кажется на первый взгляд. Нужно внедрить изменение, протестировать, понять, какой это semver-бамп, собрать и зарелизить пакет, обновить changelog. Потом то же самое для следующей версии.",[30,414,415],{},"Все усложняется еще сильнее, если хочется батчить изменения перед релизом, автоматически обновлять changelog, корректно считать semver-бамп (когда накопилось несколько изменений разного уровня) — и в итоге сделать это совместной работой команды.",[30,417,418,419,424],{},"Для этого кейса лучшим инструментом считаю ",[75,420,423],{"href":421,"rel":422},"https:\u002F\u002Fgithub.com\u002Fchangesets\u002Fchangesets",[79],"Changesets",". Он автоматизирует весь этот процесс. Достаточно на каждое изменение заполнить простую CLI-форму — а дальше релизный пайплайн можно автоматизировать так тонко, как захотите.",[67,426,428],{"id":427},"генерация-бэкенд-эндпоинтов-openapi-orval","Генерация бэкенд-эндпоинтов = OpenAPI + Orval",[30,430,431,432,437,438,443],{},"А что, если хочется полной синхронизации типов и эндпоинтов между бэкендом и фронтендом? В SSR-фреймворках можно взять ",[75,433,436],{"href":434,"rel":435},"https:\u002F\u002Ftrpc.io\u002F",[79],"tRPC"," или автотипизированный composable ",[75,439,442],{"href":440,"rel":441},"https:\u002F\u002Fnuxt.com\u002Fdocs\u002Fapi\u002Fcomposables\u002Fuse-fetch",[79],"useFetch"," из Nuxt. Но те же приемы не сработают, если у вас классическая SPA: отдельная фронтенд-команда и бэкенд на Java\u002FPHP\u002Fчем угодно еще.",[30,445,446,447,452],{},"Есть ",[75,448,451],{"href":449,"rel":450},"https:\u002F\u002Fgraphql.org\u002F",[79],"GraphQL",", но что, если нужен просто типизированный REST-клиент без перехода на GraphQL? Скорее всего, у бэкенда уже есть OpenAPI-документация под Swagger. Через codegen можно взять те же доки и сгенерировать Zod-схемы, типы и даже fetch-сервисы.",[30,454,455,456,461],{},"Лучший вариант, на мой взгляд, — ",[75,457,460],{"href":458,"rel":459},"https:\u002F\u002Forval.dev\u002F",[79],"Orval",". Это codegen, который берет OpenAPI-доки и генерирует все вышеперечисленное, плюс удобная документация и простая конфигурация.",[30,463,464],{},"Конкретный before \u002F after. На входе — небольшой OpenAPI-спек и конфиг Orval:",[179,466,470],{"className":467,"code":468,"language":469,"meta":12,"style":12},"language-yaml shiki shiki-themes github-light github-dark","# openapi.yaml\nopenapi: 3.0.3\ninfo:\n  title: Users API\n  version: 1.0.0\npaths:\n  \u002Fusers\u002F{id}:\n    get:\n      operationId: getUser\n      parameters:\n        - name: id\n          in: path\n          required: true\n          schema:\n            type: string\n      responses:\n        '200':\n          description: OK\n          content:\n            application\u002Fjson:\n              schema:\n                $ref: '#\u002Fcomponents\u002Fschemas\u002FUser'\ncomponents:\n  schemas:\n    User:\n      type: object\n      required: [id, email]\n      properties:\n        id:\n          type: string\n        email:\n          type: string\n          format: email\n","yaml",[185,471,472,478,490,498,508,519,527,535,543,554,562,576,587,598,606,617,625,633,644,652,660,668,679,687,695,703,714,732,740,748,758,766,775],{"__ignoreMap":12},[188,473,474],{"class":190,"line":191},[188,475,477],{"class":476},"sJ8bj","# openapi.yaml\n",[188,479,480,484,487],{"class":190,"line":198},[188,481,483],{"class":482},"s9eBZ","openapi",[188,485,486],{"class":194},": ",[188,488,489],{"class":201},"3.0.3\n",[188,491,492,495],{"class":190,"line":220},[188,493,494],{"class":482},"info",[188,496,497],{"class":194},":\n",[188,499,500,503,505],{"class":190,"line":234},[188,501,502],{"class":482},"  title",[188,504,486],{"class":194},[188,506,507],{"class":208},"Users API\n",[188,509,511,514,516],{"class":190,"line":510},5,[188,512,513],{"class":482},"  version",[188,515,486],{"class":194},[188,517,518],{"class":201},"1.0.0\n",[188,520,522,525],{"class":190,"line":521},6,[188,523,524],{"class":482},"paths",[188,526,497],{"class":194},[188,528,530,533],{"class":190,"line":529},7,[188,531,532],{"class":482},"  \u002Fusers\u002F{id}",[188,534,497],{"class":194},[188,536,538,541],{"class":190,"line":537},8,[188,539,540],{"class":482},"    get",[188,542,497],{"class":194},[188,544,546,549,551],{"class":190,"line":545},9,[188,547,548],{"class":482},"      operationId",[188,550,486],{"class":194},[188,552,553],{"class":208},"getUser\n",[188,555,557,560],{"class":190,"line":556},10,[188,558,559],{"class":482},"      parameters",[188,561,497],{"class":194},[188,563,565,568,571,573],{"class":190,"line":564},11,[188,566,567],{"class":194},"        - ",[188,569,570],{"class":482},"name",[188,572,486],{"class":194},[188,574,575],{"class":208},"id\n",[188,577,579,582,584],{"class":190,"line":578},12,[188,580,581],{"class":482},"          in",[188,583,486],{"class":194},[188,585,586],{"class":208},"path\n",[188,588,590,593,595],{"class":190,"line":589},13,[188,591,592],{"class":482},"          required",[188,594,486],{"class":194},[188,596,597],{"class":201},"true\n",[188,599,601,604],{"class":190,"line":600},14,[188,602,603],{"class":482},"          schema",[188,605,497],{"class":194},[188,607,609,612,614],{"class":190,"line":608},15,[188,610,611],{"class":482},"            type",[188,613,486],{"class":194},[188,615,616],{"class":208},"string\n",[188,618,620,623],{"class":190,"line":619},16,[188,621,622],{"class":482},"      responses",[188,624,497],{"class":194},[188,626,628,631],{"class":190,"line":627},17,[188,629,630],{"class":208},"        '200'",[188,632,497],{"class":194},[188,634,636,639,641],{"class":190,"line":635},18,[188,637,638],{"class":482},"          description",[188,640,486],{"class":194},[188,642,643],{"class":208},"OK\n",[188,645,647,650],{"class":190,"line":646},19,[188,648,649],{"class":482},"          content",[188,651,497],{"class":194},[188,653,655,658],{"class":190,"line":654},20,[188,656,657],{"class":482},"            application\u002Fjson",[188,659,497],{"class":194},[188,661,663,666],{"class":190,"line":662},21,[188,664,665],{"class":482},"              schema",[188,667,497],{"class":194},[188,669,671,674,676],{"class":190,"line":670},22,[188,672,673],{"class":482},"                $ref",[188,675,486],{"class":194},[188,677,678],{"class":208},"'#\u002Fcomponents\u002Fschemas\u002FUser'\n",[188,680,682,685],{"class":190,"line":681},23,[188,683,684],{"class":482},"components",[188,686,497],{"class":194},[188,688,690,693],{"class":190,"line":689},24,[188,691,692],{"class":482},"  schemas",[188,694,497],{"class":194},[188,696,698,701],{"class":190,"line":697},25,[188,699,700],{"class":482},"    User",[188,702,497],{"class":194},[188,704,706,709,711],{"class":190,"line":705},26,[188,707,708],{"class":482},"      type",[188,710,486],{"class":194},[188,712,713],{"class":208},"object\n",[188,715,717,720,722,725,727,730],{"class":190,"line":716},27,[188,718,719],{"class":482},"      required",[188,721,205],{"class":194},[188,723,724],{"class":208},"id",[188,726,139],{"class":194},[188,728,729],{"class":208},"email",[188,731,231],{"class":194},[188,733,735,738],{"class":190,"line":734},28,[188,736,737],{"class":482},"      properties",[188,739,497],{"class":194},[188,741,743,746],{"class":190,"line":742},29,[188,744,745],{"class":482},"        id",[188,747,497],{"class":194},[188,749,751,754,756],{"class":190,"line":750},30,[188,752,753],{"class":482},"          type",[188,755,486],{"class":194},[188,757,616],{"class":208},[188,759,761,764],{"class":190,"line":760},31,[188,762,763],{"class":482},"        email",[188,765,497],{"class":194},[188,767,769,771,773],{"class":190,"line":768},32,[188,770,753],{"class":482},[188,772,486],{"class":194},[188,774,616],{"class":208},[188,776,778,781,783],{"class":190,"line":777},33,[188,779,780],{"class":482},"          format",[188,782,486],{"class":194},[188,784,785],{"class":208},"email\n",[179,787,791],{"className":788,"code":789,"language":790,"meta":12,"style":12},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F orval.config.ts\nimport { defineConfig } from 'orval'\n\nexport default defineConfig({\n  api: {\n    input: '.\u002Fopenapi.yaml',\n    output: {\n      target: '.\u002Fsrc\u002Fapi\u002Fendpoints.ts',\n      client: 'fetch',\n      schemas: '.\u002Fsrc\u002Fapi\u002Fmodels',\n    },\n  },\n})\n","ts",[185,792,793,798,813,819,833,838,849,854,864,874,884,889,894],{"__ignoreMap":12},[188,794,795],{"class":190,"line":191},[188,796,797],{"class":476},"\u002F\u002F orval.config.ts\n",[188,799,800,804,807,810],{"class":190,"line":198},[188,801,803],{"class":802},"szBVR","import",[188,805,806],{"class":194}," { defineConfig } ",[188,808,809],{"class":802},"from",[188,811,812],{"class":208}," 'orval'\n",[188,814,815],{"class":190,"line":220},[188,816,818],{"emptyLinePlaceholder":817},true,"\n",[188,820,821,824,827,830],{"class":190,"line":234},[188,822,823],{"class":802},"export",[188,825,826],{"class":802}," default",[188,828,829],{"class":322}," defineConfig",[188,831,832],{"class":194},"({\n",[188,834,835],{"class":190,"line":510},[188,836,837],{"class":194},"  api: {\n",[188,839,840,843,846],{"class":190,"line":521},[188,841,842],{"class":194},"    input: ",[188,844,845],{"class":208},"'.\u002Fopenapi.yaml'",[188,847,848],{"class":194},",\n",[188,850,851],{"class":190,"line":529},[188,852,853],{"class":194},"    output: {\n",[188,855,856,859,862],{"class":190,"line":537},[188,857,858],{"class":194},"      target: ",[188,860,861],{"class":208},"'.\u002Fsrc\u002Fapi\u002Fendpoints.ts'",[188,863,848],{"class":194},[188,865,866,869,872],{"class":190,"line":545},[188,867,868],{"class":194},"      client: ",[188,870,871],{"class":208},"'fetch'",[188,873,848],{"class":194},[188,875,876,879,882],{"class":190,"line":556},[188,877,878],{"class":194},"      schemas: ",[188,880,881],{"class":208},"'.\u002Fsrc\u002Fapi\u002Fmodels'",[188,883,848],{"class":194},[188,885,886],{"class":190,"line":564},[188,887,888],{"class":194},"    },\n",[188,890,891],{"class":190,"line":578},[188,892,893],{"class":194},"  },\n",[188,895,896],{"class":190,"line":589},[188,897,898],{"class":194},"})\n",[30,900,901,902,905],{},"После ",[185,903,904],{},"orval"," получаем типизированные модели и готовый fetch-клиент — без ручного маппинга DTO:",[179,907,909],{"className":788,"code":908,"language":790,"meta":12,"style":12},"\u002F\u002F src\u002Fapi\u002Fmodels\u002Fuser.ts (сгенерировано)\nexport interface User {\n  id: string\n  email: string\n}\n\n\u002F\u002F src\u002Fapi\u002Fendpoints.ts (сгенерировано, упрощено)\nexport const getUser = async (id: string, options?: RequestInit): Promise\u003CUser> => {\n  const res = await fetch(`\u002Fusers\u002F${id}`, { ...options, method: 'GET' })\n  return res.json()\n}\n",[185,910,911,916,929,941,950,954,958,963,1022,1064,1077],{"__ignoreMap":12},[188,912,913],{"class":190,"line":191},[188,914,915],{"class":476},"\u002F\u002F src\u002Fapi\u002Fmodels\u002Fuser.ts (сгенерировано)\n",[188,917,918,920,923,926],{"class":190,"line":198},[188,919,823],{"class":802},[188,921,922],{"class":802}," interface",[188,924,925],{"class":322}," User",[188,927,928],{"class":194}," {\n",[188,930,931,935,938],{"class":190,"line":220},[188,932,934],{"class":933},"s4XuR","  id",[188,936,937],{"class":802},":",[188,939,940],{"class":201}," string\n",[188,942,943,946,948],{"class":190,"line":234},[188,944,945],{"class":933},"  email",[188,947,937],{"class":802},[188,949,940],{"class":201},[188,951,952],{"class":190,"line":510},[188,953,237],{"class":194},[188,955,956],{"class":190,"line":521},[188,957,818],{"emptyLinePlaceholder":817},[188,959,960],{"class":190,"line":529},[188,961,962],{"class":476},"\u002F\u002F src\u002Fapi\u002Fendpoints.ts (сгенерировано, упрощено)\n",[188,964,965,967,970,973,976,979,982,984,986,989,991,994,997,1000,1003,1005,1008,1011,1014,1017,1020],{"class":190,"line":537},[188,966,823],{"class":802},[188,968,969],{"class":802}," const",[188,971,972],{"class":322}," getUser",[188,974,975],{"class":802}," =",[188,977,978],{"class":802}," async",[188,980,981],{"class":194}," (",[188,983,724],{"class":933},[188,985,937],{"class":802},[188,987,988],{"class":201}," string",[188,990,139],{"class":194},[188,992,993],{"class":933},"options",[188,995,996],{"class":802},"?:",[188,998,999],{"class":322}," RequestInit",[188,1001,1002],{"class":194},")",[188,1004,937],{"class":802},[188,1006,1007],{"class":322}," Promise",[188,1009,1010],{"class":194},"\u003C",[188,1012,1013],{"class":322},"User",[188,1015,1016],{"class":194},"> ",[188,1018,1019],{"class":802},"=>",[188,1021,928],{"class":194},[188,1023,1024,1027,1030,1032,1035,1038,1041,1044,1046,1049,1052,1055,1058,1061],{"class":190,"line":545},[188,1025,1026],{"class":802},"  const",[188,1028,1029],{"class":201}," res",[188,1031,975],{"class":802},[188,1033,1034],{"class":802}," await",[188,1036,1037],{"class":322}," fetch",[188,1039,1040],{"class":194},"(",[188,1042,1043],{"class":208},"`\u002Fusers\u002F${",[188,1045,724],{"class":194},[188,1047,1048],{"class":208},"}`",[188,1050,1051],{"class":194},", { ",[188,1053,1054],{"class":802},"...",[188,1056,1057],{"class":194},"options, method: ",[188,1059,1060],{"class":208},"'GET'",[188,1062,1063],{"class":194}," })\n",[188,1065,1066,1069,1072,1074],{"class":190,"line":556},[188,1067,1068],{"class":802},"  return",[188,1070,1071],{"class":194}," res.",[188,1073,183],{"class":322},[188,1075,1076],{"class":194},"()\n",[188,1078,1079],{"class":190,"line":564},[188,1080,237],{"class":194},[30,1082,1083],{},"Единственная проблема, которую стоит упомянуть: нужно аккуратно выстраивать совместную работу — иначе типы на фронте могут внезапно сломаться (например, вы пишете хотфикс, а бэкенд-команда уже задеплоила фичу на dev, и ваши типы разъехались). Один из вариантов — вынести OpenAPI-схемы в npm-пакет, который обе команды обновляют и версионируют вместе.",[67,1085,1087],{"id":1086},"генерация-токенов-из-figma-tokens-studio-style-dictionary","Генерация токенов из Figma = Tokens Studio + Style Dictionary",[30,1089,1090],{},"Это одновременно самая полезная и самая сложная в настройке автоматизация — потому что придется менять то, как работает дизайн-команда.",[30,1092,1093],{},"Главная боль при готовом UI — держать его в синхроне с изменениями в Figma. Слышал кучу страшных историй, как команды тратят даже 1\u002F5 спринта только на синхронизацию с Figma. Версионирование в Figma до сих пор слабое, поэтому появляется куча локальных костылей.",[30,1095,1096,1097,1102,1103,1108],{},"Эту проблему можно закрыть через ",[75,1098,1101],{"href":1099,"rel":1100},"https:\u002F\u002Ftokens.studio\u002F",[79],"Tokens Studio",". По сути это плагин для Figma для управления дизайн-токенами. Когда дизайнеры работают через него, токены можно экспортировать как схему (что-то вроде OpenAPI, но для токенов), а дальше прогнать через ",[75,1104,1107],{"href":1105,"rel":1106},"https:\u002F\u002Fstyledictionary.com\u002F",[79],"Style Dictionary"," и получить ваш конкретный CSS-сетап — Tailwind, CSS-переменные, Sass и так далее.",[30,1110,1111],{},"Конкретный before \u002F after. Дизайнеры экспортируют схему Tokens Studio, а вы указываете на неё Style Dictionary:",[179,1113,1115],{"className":181,"code":1114,"language":183,"meta":12,"style":12},"\u002F\u002F tokens\u002Fcolors.json (экспорт из Tokens Studio)\n{\n  \"color\": {\n    \"brand\": {\n      \"primary\": { \"value\": \"#2563eb\", \"type\": \"color\" },\n      \"secondary\": { \"value\": \"#7c3aed\", \"type\": \"color\" }\n    },\n    \"surface\": {\n      \"bg\": { \"value\": \"#ffffff\", \"type\": \"color\" },\n      \"fg\": { \"value\": \"#0f172a\", \"type\": \"color\" }\n    }\n  },\n  \"spacing\": {\n    \"sm\": { \"value\": \"8\", \"type\": \"spacing\" },\n    \"md\": { \"value\": \"16\", \"type\": \"spacing\" },\n    \"lg\": { \"value\": \"24\", \"type\": \"spacing\" }\n  }\n}\n",[185,1116,1117,1122,1126,1134,1141,1170,1195,1199,1206,1230,1254,1259,1263,1270,1295,1319,1343,1348],{"__ignoreMap":12},[188,1118,1119],{"class":190,"line":191},[188,1120,1121],{"class":476},"\u002F\u002F tokens\u002Fcolors.json (экспорт из Tokens Studio)\n",[188,1123,1124],{"class":190,"line":198},[188,1125,195],{"class":194},[188,1127,1128,1131],{"class":190,"line":220},[188,1129,1130],{"class":201},"  \"color\"",[188,1132,1133],{"class":194},": {\n",[188,1135,1136,1139],{"class":190,"line":234},[188,1137,1138],{"class":201},"    \"brand\"",[188,1140,1133],{"class":194},[188,1142,1143,1146,1149,1152,1154,1157,1159,1162,1164,1167],{"class":190,"line":510},[188,1144,1145],{"class":201},"      \"primary\"",[188,1147,1148],{"class":194},": { ",[188,1150,1151],{"class":201},"\"value\"",[188,1153,486],{"class":194},[188,1155,1156],{"class":208},"\"#2563eb\"",[188,1158,139],{"class":194},[188,1160,1161],{"class":201},"\"type\"",[188,1163,486],{"class":194},[188,1165,1166],{"class":208},"\"color\"",[188,1168,1169],{"class":194}," },\n",[188,1171,1172,1175,1177,1179,1181,1184,1186,1188,1190,1192],{"class":190,"line":521},[188,1173,1174],{"class":201},"      \"secondary\"",[188,1176,1148],{"class":194},[188,1178,1151],{"class":201},[188,1180,486],{"class":194},[188,1182,1183],{"class":208},"\"#7c3aed\"",[188,1185,139],{"class":194},[188,1187,1161],{"class":201},[188,1189,486],{"class":194},[188,1191,1166],{"class":208},[188,1193,1194],{"class":194}," }\n",[188,1196,1197],{"class":190,"line":529},[188,1198,888],{"class":194},[188,1200,1201,1204],{"class":190,"line":537},[188,1202,1203],{"class":201},"    \"surface\"",[188,1205,1133],{"class":194},[188,1207,1208,1211,1213,1215,1217,1220,1222,1224,1226,1228],{"class":190,"line":545},[188,1209,1210],{"class":201},"      \"bg\"",[188,1212,1148],{"class":194},[188,1214,1151],{"class":201},[188,1216,486],{"class":194},[188,1218,1219],{"class":208},"\"#ffffff\"",[188,1221,139],{"class":194},[188,1223,1161],{"class":201},[188,1225,486],{"class":194},[188,1227,1166],{"class":208},[188,1229,1169],{"class":194},[188,1231,1232,1235,1237,1239,1241,1244,1246,1248,1250,1252],{"class":190,"line":556},[188,1233,1234],{"class":201},"      \"fg\"",[188,1236,1148],{"class":194},[188,1238,1151],{"class":201},[188,1240,486],{"class":194},[188,1242,1243],{"class":208},"\"#0f172a\"",[188,1245,139],{"class":194},[188,1247,1161],{"class":201},[188,1249,486],{"class":194},[188,1251,1166],{"class":208},[188,1253,1194],{"class":194},[188,1255,1256],{"class":190,"line":564},[188,1257,1258],{"class":194},"    }\n",[188,1260,1261],{"class":190,"line":578},[188,1262,893],{"class":194},[188,1264,1265,1268],{"class":190,"line":589},[188,1266,1267],{"class":201},"  \"spacing\"",[188,1269,1133],{"class":194},[188,1271,1272,1275,1277,1279,1281,1284,1286,1288,1290,1293],{"class":190,"line":600},[188,1273,1274],{"class":201},"    \"sm\"",[188,1276,1148],{"class":194},[188,1278,1151],{"class":201},[188,1280,486],{"class":194},[188,1282,1283],{"class":208},"\"8\"",[188,1285,139],{"class":194},[188,1287,1161],{"class":201},[188,1289,486],{"class":194},[188,1291,1292],{"class":208},"\"spacing\"",[188,1294,1169],{"class":194},[188,1296,1297,1300,1302,1304,1306,1309,1311,1313,1315,1317],{"class":190,"line":608},[188,1298,1299],{"class":201},"    \"md\"",[188,1301,1148],{"class":194},[188,1303,1151],{"class":201},[188,1305,486],{"class":194},[188,1307,1308],{"class":208},"\"16\"",[188,1310,139],{"class":194},[188,1312,1161],{"class":201},[188,1314,486],{"class":194},[188,1316,1292],{"class":208},[188,1318,1169],{"class":194},[188,1320,1321,1324,1326,1328,1330,1333,1335,1337,1339,1341],{"class":190,"line":619},[188,1322,1323],{"class":201},"    \"lg\"",[188,1325,1148],{"class":194},[188,1327,1151],{"class":201},[188,1329,486],{"class":194},[188,1331,1332],{"class":208},"\"24\"",[188,1334,139],{"class":194},[188,1336,1161],{"class":201},[188,1338,486],{"class":194},[188,1340,1292],{"class":208},[188,1342,1194],{"class":194},[188,1344,1345],{"class":190,"line":627},[188,1346,1347],{"class":194},"  }\n",[188,1349,1350],{"class":190,"line":635},[188,1351,237],{"class":194},[179,1353,1357],{"className":1354,"code":1355,"language":1356,"meta":12,"style":12},"language-js shiki shiki-themes github-light github-dark","\u002F\u002F style-dictionary.config.js\nexport default {\n  source: ['tokens\u002F**\u002F*.json'],\n  platforms: {\n    css: {\n      transformGroup: 'css',\n      buildPath: 'build\u002F',\n      files: [\n        {\n          destination: 'theme.css',\n          format: 'css\u002Fvariables',\n          options: {\n            selector: '@theme',\n          },\n        },\n      ],\n    },\n  },\n}\n","js",[185,1358,1359,1364,1372,1382,1387,1392,1402,1412,1417,1422,1432,1442,1447,1457,1462,1467,1472,1476,1480],{"__ignoreMap":12},[188,1360,1361],{"class":190,"line":191},[188,1362,1363],{"class":476},"\u002F\u002F style-dictionary.config.js\n",[188,1365,1366,1368,1370],{"class":190,"line":198},[188,1367,823],{"class":802},[188,1369,826],{"class":802},[188,1371,928],{"class":194},[188,1373,1374,1377,1380],{"class":190,"line":220},[188,1375,1376],{"class":194},"  source: [",[188,1378,1379],{"class":208},"'tokens\u002F**\u002F*.json'",[188,1381,217],{"class":194},[188,1383,1384],{"class":190,"line":234},[188,1385,1386],{"class":194},"  platforms: {\n",[188,1388,1389],{"class":190,"line":510},[188,1390,1391],{"class":194},"    css: {\n",[188,1393,1394,1397,1400],{"class":190,"line":521},[188,1395,1396],{"class":194},"      transformGroup: ",[188,1398,1399],{"class":208},"'css'",[188,1401,848],{"class":194},[188,1403,1404,1407,1410],{"class":190,"line":529},[188,1405,1406],{"class":194},"      buildPath: ",[188,1408,1409],{"class":208},"'build\u002F'",[188,1411,848],{"class":194},[188,1413,1414],{"class":190,"line":537},[188,1415,1416],{"class":194},"      files: [\n",[188,1418,1419],{"class":190,"line":545},[188,1420,1421],{"class":194},"        {\n",[188,1423,1424,1427,1430],{"class":190,"line":556},[188,1425,1426],{"class":194},"          destination: ",[188,1428,1429],{"class":208},"'theme.css'",[188,1431,848],{"class":194},[188,1433,1434,1437,1440],{"class":190,"line":564},[188,1435,1436],{"class":194},"          format: ",[188,1438,1439],{"class":208},"'css\u002Fvariables'",[188,1441,848],{"class":194},[188,1443,1444],{"class":190,"line":578},[188,1445,1446],{"class":194},"          options: {\n",[188,1448,1449,1452,1455],{"class":190,"line":589},[188,1450,1451],{"class":194},"            selector: ",[188,1453,1454],{"class":208},"'@theme'",[188,1456,848],{"class":194},[188,1458,1459],{"class":190,"line":600},[188,1460,1461],{"class":194},"          },\n",[188,1463,1464],{"class":190,"line":608},[188,1465,1466],{"class":194},"        },\n",[188,1468,1469],{"class":190,"line":619},[188,1470,1471],{"class":194},"      ],\n",[188,1473,1474],{"class":190,"line":627},[188,1475,888],{"class":194},[188,1477,1478],{"class":190,"line":635},[188,1479,893],{"class":194},[188,1481,1482],{"class":190,"line":646},[188,1483,237],{"class":194},[30,1485,1486,1487,1490],{},"С ",[185,1488,1489],{},"selector: '@theme'"," Style Dictionary сразу генерирует theme-файл для Tailwind v4:",[179,1492,1496],{"className":1493,"code":1494,"language":1495,"meta":12,"style":12},"language-css shiki shiki-themes github-light github-dark","\u002F**\n * Do not edit directly, this file was auto-generated.\n *\u002F\n\n@theme {\n  --color-brand-primary: #2563eb;\n  --color-brand-secondary: #7c3aed;\n  --color-surface-bg: #ffffff;\n  --color-surface-fg: #0f172a;\n  --spacing-sm: 8;\n  --spacing-md: 16;\n  --spacing-lg: 24;\n}\n","css",[185,1497,1498,1503,1508,1513,1517,1524,1536,1546,1554,1564,1569,1574,1579],{"__ignoreMap":12},[188,1499,1500],{"class":190,"line":191},[188,1501,1502],{"class":476},"\u002F**\n",[188,1504,1505],{"class":190,"line":198},[188,1506,1507],{"class":476}," * Do not edit directly, this file was auto-generated.\n",[188,1509,1510],{"class":190,"line":220},[188,1511,1512],{"class":476}," *\u002F\n",[188,1514,1515],{"class":190,"line":234},[188,1516,818],{"emptyLinePlaceholder":817},[188,1518,1519,1522],{"class":190,"line":510},[188,1520,1521],{"class":802},"@theme",[188,1523,928],{"class":194},[188,1525,1526,1529,1533],{"class":190,"line":521},[188,1527,1528],{"class":194},"  --color-brand-primary: ",[188,1530,1532],{"class":1531},"s7hpK","#2563eb",[188,1534,1535],{"class":194},";\n",[188,1537,1538,1541,1544],{"class":190,"line":529},[188,1539,1540],{"class":194},"  --color-brand-secondary: ",[188,1542,1543],{"class":1531},"#7c3aed",[188,1545,1535],{"class":194},[188,1547,1548,1551],{"class":190,"line":537},[188,1549,1550],{"class":194},"  --color-surface-bg: ",[188,1552,1553],{"class":1531},"#ffffff;\n",[188,1555,1556,1559,1562],{"class":190,"line":545},[188,1557,1558],{"class":194},"  --color-surface-fg: ",[188,1560,1561],{"class":1531},"#0f172a",[188,1563,1535],{"class":194},[188,1565,1566],{"class":190,"line":556},[188,1567,1568],{"class":194},"  --spacing-sm: 8;\n",[188,1570,1571],{"class":190,"line":564},[188,1572,1573],{"class":194},"  --spacing-md: 16;\n",[188,1575,1576],{"class":190,"line":578},[188,1577,1578],{"class":194},"  --spacing-lg: 24;\n",[188,1580,1581],{"class":190,"line":589},[188,1582,237],{"class":194},[179,1584,1586],{"className":1493,"code":1585,"language":1495,"meta":12,"style":12},"\u002F* app.css *\u002F\n@import \"tailwindcss\";\n@import \".\u002Fbuild\u002Ftheme.css\";\n",[185,1587,1588,1593,1603],{"__ignoreMap":12},[188,1589,1590],{"class":190,"line":191},[188,1591,1592],{"class":476},"\u002F* app.css *\u002F\n",[188,1594,1595,1598,1601],{"class":190,"line":198},[188,1596,1597],{"class":802},"@import",[188,1599,1600],{"class":208}," \"tailwindcss\"",[188,1602,1535],{"class":194},[188,1604,1605,1607,1610],{"class":190,"line":220},[188,1606,1597],{"class":802},[188,1608,1609],{"class":208}," \".\u002Fbuild\u002Ftheme.css\"",[188,1611,1535],{"class":194},[30,1613,1614],{},"С версионированием токенов та же история, но Tokens Studio умеет интегрироваться с Git — дизайнеры могут нормально версионировать изменения. Дальше можно сделать npm-пакет, где эта генерация применяется, и закрепить фронтенд на конкретной версии.",[25,1616,1618],{"id":1617},"заключение","Заключение",[30,1620,1621],{},"Я постарался собрать любимые автоматизации с реальным импактом — те, которыми продолжал пользоваться после всех экспериментов. Надеюсь, вы нашли для себя что-то интересное и новое!",[1623,1624,1625],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}",{"title":12,"searchDepth":198,"depth":198,"links":1627},[1628,1629,1630,1640],{"id":27,"depth":198,"text":28},{"id":38,"depth":198,"text":39},{"id":61,"depth":198,"text":62,"children":1631},[1632,1633,1634,1635,1636,1637,1638,1639],{"id":69,"depth":220,"text":70},{"id":148,"depth":220,"text":149},{"id":168,"depth":220,"text":169},{"id":297,"depth":220,"text":298},{"id":359,"depth":220,"text":360},{"id":408,"depth":220,"text":409},{"id":427,"depth":220,"text":428},{"id":1086,"depth":220,"text":1087},{"id":1617,"depth":198,"text":1618},"2026-07-21 00:00:00 UTC","Полезные автоматизации, которые заметно улучшают DX","10 мин","md",{},"\u002Fposts\u002Fessential-frontend-automations",null,{"title":5,"description":1642},{"loc":1646},"posts\u002Fessential-frontend-automations",[1652],"dev","iEvSmCbRSlXOKkGQPdj4lsEer0xSmIz29hJICcc18bE",1790706793960]