Дарк и баги
Однажды я захотел сделать свой собственный юзерскрипт. Для тех кто не в теме, юзерскрипты aka пользовательские скрипты это браузерные плагины на минималках. Они внедряются в определённые страницы и меняют их в угоду пользователя.
Разумеется, для такого нужен браузерный плагин для юзерскриптов — TamperMonkey там или GreasyMonkey... Но я пошёл путём нейтрала и выбрал опенсурсный ViolentMonkey. Среди всего прочего, там предоставлялся шаблон юзерскрипта по феншую.
Сначала всё шло достаточно хорошо, код пишется, нейронные связи нарабатываются... Но что-то мне не давало покоя и я решил туда впилить иконки.
В комплекте с шаблоном шла такая штука как unocss, о которой я тогда знал почти ничего. А у неё в комплекте шёл шаблон для того чтобы пилить иконки на чистом css. Включался он буквально одной строчкой в конфиге:
presets: [presetIcons(), ...]
Разумеется я довольный прописал её, но оно не заработало.
"Ладно," — подумал я. — "Я дурачок и где-то ошибся."
И в самом деле, я скоро выяснил, что надо ещё дополнительно поставить источник иконок. После чего всё заработало, я довольный пошёл разбираться с css и прочим.
Во время разработки используется специальный режим, который отслеживает изменения и пересобирает их на лету. Таким образом ты всё время работаешь с актуальной версией. Удобно. Но когда я решил собрать релизную версию, иконки не заработали.
"Следствие вели" с Дарком
Тут я начал присматриваться к тому что у меня работает. Выяснилось, что если просто запустить режим разработки, то он тоже не добавит иконку. Но если что-то сохранить, то при пересборке иконка добавится.
Начинаю разбираться как это работает. Вкратце: unocss построен поверх "правил". Правило это, грубо говоря, класс css. Поверх этого у нас есть плагин transformDirectives
, который преобразовывает некоторые директивы в код. В итоге мы имеем такой файлик css:
@unocss; .test { @apply i-mdi-add; @apply font-size-7; }
В данном случае @unocss
и @apply
— директивы, а i-mdi-add
и font-size-7
— правила. В итоге мы после обработки должны получить что-то такое:
.test { /* код для иконки */ font-size: 1.5rem; }
Вот только по факту мы получаем такое:
.test { @apply i-mdi-add; font-size: 1.5rem; }
И тут я заметил, что в конфиге то у меня не был указан плагин transformDirectives
. Странно. А как оно тогда работало? В любом случае. добавление плагина ни на что не повлияло, значит дело в чём-то другом.
Другим моментиком стало, что если врубить отображение отладочных сообщений (DEBUG=* yourcommand
очень полезная вещь), то мы увидим такое:
Иконка загружается уже после того, как мы собрали нужный юзерскрипт. Это уже явно бред. Тут мне стало понятно, что проблема вряд ли в моём коде, а значит пора идти разгребать конфиги и сорцы.
Первым делом я полез узнавать, чем я вообще собираю всю эту катавасию. Оказалось rollup. Отлично, идём в rollup.config.mjs
и пугаемся его размеров
import { defineExternal, definePlugins } from '@gera2ld/plaid-rollup'; import { defineConfig } from 'rollup'; import userscript from 'rollup-plugin-userscript'; import pkg from './package.json' assert { type: 'json' }; export default defineConfig(Object.entries({ 'userscript': 'src/userscript/index.ts', }).map(([name, entry]) => ({ input: entry, plugins: [ ...definePlugins({ esm: true, minimize: false, postcss: { inject: false, minimize: true, }, extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'], }), userscript((meta) => meta.replace('process.env.AUTHOR', pkg.author)), ], external: defineExternal([ '@violentmonkey/ui', '@violentmonkey/dom', // 'solid-js', // 'solid-js/web', ]), output: { format: 'iife', file: `dist/${name}.user.js`, banner: `(async () => {`, footer: `})();`, globals: { 'solid-js': 'await import("https://esm.sh/solid-js")', 'solid-js/web': 'await import("https://esm.sh/solid-js/web")', '@violentmonkey/dom': 'VM', '@violentmonkey/ui': 'VM', }, indent: false, }, })));
Ого, тут парочка кастомных плагинов. Подозрительно. Сначала жертвой пал rollup-plugin-userscript
впрочем, он просто добавлял заголовки для юзерскрипта. Вердикт: не виновен.
Дальше у нас падает взгляд на огромную штуку из @gera2ld/plaid-rollup
. Вот там уже есть где разбежаться, она импортит кучу плагинов. babel, postcss, terser и ещё парочку своих.
После долгих и нудных раборок с бабелем я решил вынести вердикт "не виновен (пока что)", но как только я заглянул в конфиг postcss
, всё начало проясняться:
module.exports = { plugins: { 'postcss-calc': {}, 'postcss-nested': {}, '@unocss/postcss': {}, }, };
Логичным вопросом будет "а что это за @unocss/postcss
, не он ли виновник?" и как мы быстро обнаруживаем что этот плагин действительно заменяет функциональность transformDirectives
. Но почему она не работает?
// @ts-expect-error types doesn't allow async callback but it seems work root.walkAtRules(directiveName, async (rule) => {
Как говорится, никогда не спрашивай женщину о её возрасте, мужчину о его зарплате и веб разработчика why it seems work.
Bugs dies twice
Как настоящий джентльмен, я решил отправить им ишью, мол на самом деле это не работает. Для этого всего то навсего надо сделать минимальный воспроизводимый пример. Минимальный в смысле там должен быть баг, только баг и ничего кроме бага.
Понятное дело, иконки совсем не обязательный элемент... скорее всего. Легким взмахом руки мы узнаем что есть асинхронные правила. Поскольку иконки грузятся с диска, то вызываемая задержка видимо и заставляет наши правила перестать работать.
Прекрасно, берём postcss-cli и unocss с плагинчиком и пишем асинхронное правило с задержкой на 1 секунду.
На этом моменте я реши сдаться и пойти с другой стороны. Мееедленно аккуратно выпиливая части системы. Под нож попало всё — и babel, и куча плагинов и в конце концов rollup.
И тогда стало понятно что для бага надо ещё одно маааленькое, но важное условие. Наличие в файле директивы @unocss
. Я не знаю почему и как, но если у неё его нет, то всё работает. Иначе не работает.
Мораль
морали нет, ишью на гитхабе, пул реквест с фиксом там же. ждём одобрения