惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
The Last Watchdog
The Last Watchdog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
T
Troy Hunt's Blog
L
LINUX DO - 最新话题
C
Check Point Blog
T
Threat Research - Cisco Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
V
Vulnerabilities – Threatpost
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
J
Java Code Geeks
Apple Machine Learning Research
Apple Machine Learning Research
大猫的无限游戏
大猫的无限游戏
S
Security @ Cisco Blogs
IT之家
IT之家
T
The Exploit Database - CXSecurity.com
The GitHub Blog
The GitHub Blog
D
Docker
Engineering at Meta
Engineering at Meta
AWS News Blog
AWS News Blog
S
Security Affairs
U
Unit 42
P
Palo Alto Networks Blog
V
Visual Studio Blog
Y
Y Combinator Blog
D
DataBreaches.Net
Forbes - Security
Forbes - Security
阮一峰的网络日志
阮一峰的网络日志
美团技术团队
Security Latest
Security Latest
aimingoo的专栏
aimingoo的专栏
Simon Willison's Weblog
Simon Willison's Weblog
A
Arctic Wolf
博客园_首页
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
H
Hacker News: Front Page
博客园 - 司徒正美
博客园 - Franky
宝玉的分享
宝玉的分享
TaoSecurity Blog
TaoSecurity Blog
Latest news
Latest news
Scott Helme
Scott Helme
MongoDB | Blog
MongoDB | Blog
量子位
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cisco Blogs
P
Privacy International News Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Как я сделал «клик по элементу → открыть в VS Code» за один вечер
sozercanie_kosmosa · 2026-05-26 · via Все публикации подряд на Хабре

Простой

6 мин

8K

Началось всё банально. Зашёл коллега, говорит: «Где у нас хлебные крошки в шапке лежат?». Проект — около 150 компонентов, всё именуется по-своему, структура папок местами загадочная. Я начал тыкать в React DevTools, искать по тексту «Breadcrumb» в файлах… В общем, минут через пять нашёл. Это в очередной раз раздражало.

Так появился vite-plugin-debug-meta — плагин, который позволяет зажать Ctrl + Shift, навести на любой элемент в браузере, кликнуть — и нужный файл откроется в редакторе на нужной строке.

npm install -D vite-plugin-debug-meta

Сначала я потратил час на поиск готового решения

Если честно, я был уверен, что такое уже сто раз написали. Порылся в npm — есть react-dev-inspector, есть vite-plugin-react-inspector… Смотришь на зависимости — там тянется половина вселенной, или нужен специальный babel-preset, или работает только с определённой версией React. Для нашего проекта ничего не подошло без плясок.

В итоге решил написать своё. Оказалось, что базовую версию реально сделать за один вечер. Вот что из этого вышло.


Секрет, о котором я сам не знал год

Первое открытие — у Vite из коробки есть эндпоинт /__open-in-editor. Честно, я работал с Vite больше года и понятия не имел о его существовании. Его используют Vue DevTools и кое-что ещё под капотом, но нигде особо не афишируют.

Работает просто:

/__open-in-editor?file=src/components/Header.tsx:42:1

Vite принимает этот запрос в режиме npm run dev и через пакет launch-editor (его написал Эван Ю) открывает ваш редактор на нужной строке. Он сам пытается угадать запущенную IDE, а если не угадывает — читает переменную LAUNCH_EDITOR.

Попробуйте прямо сейчас в консоли вашего Vite-проекта:

fetch('/__open-in-editor?file=src/App.tsx:1:1')

Скорее всего, VS Code (или что у вас там) уже открылся. Это магия, и именно на ней держится весь плагин — никаких вебсокетов, никакого отдельного сервера.


Что плагин делает и как

Идея в двух словах:

  1. На этапе сборки Babel обходит все .tsx/.jsx файлы и добавляет к каждому JSX-тегу два атрибута: data-debug-file (путь к файлу + номер строки) и data-debug-component (имя компонента). Это происходит только в dev-режиме, в продакшн это не попадает.

  2. В HTML-страницу инжектируется маленький скрипт, который слушает Ctrl + Shift. Пока зажаты — в браузере включается режим инспекции: элементы подсвечиваются зелёной рамкой, над ними появляется бейджик с именем компонента и его размерами. Кликаете — файл открывается в редакторе.


Часть 1. Трансформация JSX

Это Vite-плагин, поэтому нам нужен хук transform. Для разбора JSX берём три пакета из Babel-экосистемы: @babel/parser, @babel/traverse, @babel/generator.

transform(code: string, id: string) {
  // Только наши файлы, без node_modules
  if (!/\.(tsx|jsx)$/.test(id) || id.includes("node_modules")) return null;

  const relativePath = path.relative(process.cwd(), id).replace(/\\/g, "/");
  const componentName = path.basename(id).replace(/\.(tsx|jsx)$/, "");

  const ast = parser.parse(code, {
    sourceType: "module",
    plugins: ["typescript", "jsx", "decorators-legacy"],
  });

  traverse(ast, {
    JSXOpeningElement(nodePath: any) {
      const nameNode = nodePath.node.name;

      // Фрагменты пропускаем — у них нет DOM-узла, туда атрибут не суёшь
      const isFragment =
        (nameNode.type === "JSXIdentifier" && nameNode.name === "Fragment") ||
        (nameNode.type === "JSXMemberExpression" &&
          nameNode.object.name === "React" &&
          nameNode.property.name === "Fragment");

      if (isFragment) return;

      const line = nodePath.node.loc?.start.line ?? 1;
      const debugFile = `${relativePath}#${line}`;

      // Не добавляем дубли, если атрибут уже есть
      const hasAttr = nodePath.node.attributes.some(
        (attr: any) => attr.type === "JSXAttribute" && attr.name.name === "data-debug-file"
      );
      if (hasAttr) return;

      const makeAttr = (name: string, value: string) => ({
        type: "JSXAttribute" as const,
        name: { type: "JSXIdentifier" as const, name },
        value: {
          type: "JSXExpressionContainer" as const,
          expression: { type: "StringLiteral" as const, value }
        }
      });

      const attrs = [makeAttr("data-debug-file", debugFile), makeAttr("data-debug-component", componentName)];

      // Если есть spread-пропсы (...props), вставляем ДО них,
      // иначе пользователь может случайно их перезаписать своими data-* 
      const spreadIndex = nodePath.node.attributes.findIndex(
        (attr: any) => attr.type === "JSXSpreadAttribute"
      );

      if (spreadIndex !== -1) {
        nodePath.node.attributes.splice(spreadIndex, 0, ...attrs);
      } else {
        nodePath.node.attributes.push(...attrs);
      }
    }
  });

  const result = generate(ast, { sourceMaps: true, sourceFileName: id }, code);
  return { code: result.code, map: result.map };
}

Небольшая тонкость со spread-атрибутами: если у компонента есть {...props}, а где-то выше по коду в props попадает data-debug-file, то наш атрибут будет перезаписан. Поэтому вставляем до spread-а — тогда явный {...props} выиграет, но случайной перезаписи не будет.


Часть 2. Инъекция скрипта и стилей

Тут всё просто — используем хук transformIndexHtml, который позволяет добавить теги в <head> страницы:

apply: "serve", // плагин не трогает продакшн-сборку вообще
transformIndexHtml() {
  return [
    {
      tag: "script",
      attrs: { type: "text/javascript" },
      children: inspectorScript,
      injectTo: "head",
    },
    {
      tag: "style",
      children: `
        /* В режиме инспекции disabled-элементы не перехватывают клики */
        .debug-inspect-mode [disabled],
        .debug-inspect-mode :disabled,
        .debug-inspect-mode .disabled {
          pointer-events: none !important;
        }
        /* Подсвечиваем самый вложенный элемент под курсором */
        .debug-inspect-mode [data-debug-file]:hover:not(:has([data-debug-file]:hover)) {
          outline: 2px solid #10B981 !important;
          outline-offset: -2px !important;
          cursor: crosshair !important;
        }
      `.trim(),
      injectTo: "head",
    }
  ];
}

Селектор :not(:has([data-debug-file]:hover)) — это «выбери элемент с атрибутом, у которого нет дочернего элемента с тем же атрибутом под курсором». Немного по-читерски, зато работает чисто и без JS. Браузерная поддержка у :has() уже вполне приличная для dev-инструментов.


Часть 3. Проблема «я кликнул на кнопку, а хочу попасть на страницу»

Вот здесь было самое интересное.

Допустим, у вас на DashboardPage.tsx стоит кнопка <SaveButton />. Вы кликаете по ней с Ctrl + Shift. DOM-атрибут data-debug-file укажет на SaveButton.tsx — потому что именно там находится <button> в JSX. Но вы-то хотели попасть в DashboardPage.tsx, чтобы поменять текст или убрать кнопку!

Решение — читать React Fiber. React хранит внутреннее дерево компонентов прямо в DOM-узлах, в свойстве с именем вида __reactFiber$xxxx. Пройдя по дереву Fiber вверх (curr.return), можно собрать полный стек: SaveButton → DashboardPage → App → ...

// Ищем Fiber, начиная с кликнутого элемента и поднимаясь выше по DOM
var fiber = null;
var currentEl = e.target;
while (currentEl && !fiber) {
  var keys = Object.keys(currentEl);
  for (var i = 0; i < keys.length; i++) {
    if (keys[i].startsWith("__reactFiber$") || keys[i].startsWith("__reactInternalInstance$")) {
      fiber = currentEl[keys[i]];
      break;
    }
  }
  if (!fiber) currentEl = currentEl.parentElement;
}

// Собираем стек компонентов
var trace = [];
var curr = fiber;
while (curr) {
  if (curr._debugSource) {
    var src = curr._debugSource;
    var fileName = src.fileName.replace(/\\/g, "/");

    if (!fileName.includes("node_modules")) {
      var srcIndex = fileName.indexOf("/src/");
      var relativePath = srcIndex !== -1 ? fileName.slice(srcIndex + 1) : fileName;
      var compName = curr.type?.displayName || curr.type?.name || "Unknown";

      trace.push({ relativePath, line: src.lineNumber, component: compName });
    }
  }
  curr = curr.return;
}

Дальше простая эвристика: берём первый элемент стека (самый глубокий) и идём вверх. Как только файл меняется — вот это и есть место использования. Туда и открываем редактор.

В консоли при этом печатается полный стек — удобно, когда хочешь понять всю цепочку:

🔍 Debug Inspect:
👉 <SaveButton> at src/components/SaveButton.tsx:12
   <DashboardPage> at src/pages/DashboardPage.tsx:47
   <Router> at src/App.tsx:23

Часть 4. Бейдж при наведении

Когда зажаты Ctrl + Shift и курсор движется по странице, хочется сразу видеть, на что ты смотришь. Делаем плавающий бейджик:

function updateBadge(el) {
  if (!el) { badge.style.display = "none"; return; }

  var rect = el.getBoundingClientRect();
  var compName = el.getAttribute("data-debug-component") || "";
  var size = Math.round(rect.width) + " × " + Math.round(rect.height);

  badge.textContent = compName ? compName + " | " + size : size;

  var top = rect.top - 24;
  badge.style.top = (top < 0 ? rect.top + 4 : top) + "px";
  badge.style.left = Math.max(rect.left, 4) + "px";
  badge.style.display = "block";
}

Выглядит так: HeaderNav | 1280 × 64. Элемент при этом подсвечен зелёным outline. На самом деле это сильно помогает понять реальные размеры блоков без открытия DevTools.


Установка и использование

npm install -D vite-plugin-debug-meta
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { debugMetaPlugin } from "vite-plugin-debug-meta";

export default defineConfig({
  plugins: [
    react(),
    debugMetaPlugin({
      editor: "code" // "webstorm", "cursor", "rider", "zed" и т.д.
    })
  ]
});

Параметр editor нужен, если Vite не угадывает вашу IDE автоматически. Под капотом он просто выставляет переменную LAUNCH_EDITOR — вы можете сделать то же самое в .env.local, если не хотите трогать конфиг.


Несколько вещей, которые я понял в процессе

_debugSource работает только если React скомпилирован в режиме development. В обычном Vite + React это так по умолчанию, но если вы где-то принудительно выставили NODE_ENV=production, Fiber не будет содержать информацию об источнике.

Фрагменты надо явно пропускать. Если добавить data-debug-file к <React.Fragment> или <>...</>, React бросает ошибку: фрагменты не принимают произвольные пропсы. Потратил на это минут двадцать.

Со spread-атрибутами надо быть аккуратным. Если компонент принимает {...props} и при этом кто-то снаружи передаёт data-debug-file — наш атрибут перезапишется. Это редкий кейс, но он существует.


Итого

Плагин работает только в dev-режиме (apply: "serve"), ничего лишнего в продакшн не попадает. Основные зависимости — три пакета Babel для парсинга AST, всё остальное уже есть в Vite.

Код открыт на GitHub под лицензией MIT — sozercaniekosmosa/vite-plugin-debug-meta. Если у вас похожая боль с навигацией по большим React-проектам — попробуйте, возможно пригодится.