Дарк и баги
Однажды я захотел сделать свой собственный юзерскрипт. Для тех кто не в теме, юзерскрипты 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. Я не знаю почему и как, но если у неё его нет, то всё работает. Иначе не работает.
Мораль
морали нет, ишью на гитхабе, пул реквест с фиксом там же. ждём одобрения