Русский
Русский
English
Статистика
Реклама

Из песочницы Как я выкинул webpack и написал babel-plugin для транспила scsssass

Предыстория


Как-то субботним вечером я сидел и искал способы сборки UI-Kit с помощью webpack. В качестве демо UI-kit я пользуюсь styleguidst. Конечно же, webpack умный и все файлы, которые есть в рабочем каталоге он запихивает в один бандл и оттуда всё крутится и вертится.

Я создал файл entry.js, импортнул туда все компоненты, затем оттуда же экспортнул. Вроде всё ок.

import Button from 'components/Button'import Dropdown from 'components/Dropdown 'export {  Button,  Dropdown }

И после сборки всего этого, я получил на выходе output.js, в котором как и ожидалось было всё все компоненты в куче в одном файле. Тут возник вопрос:
А как мне собрать все кнопочки, дропдауны и прочее по отдельности, что бы импортировать в других проектах?
А я ведь хочу это ещё и в npm залить как пакет.

Хм Поехали по порядку.

Multiple entries


Конечно, первая идея, которая может прийти в голову спарсить все компоненты в рабочем каталоге. Пришлось немножко погуглить про парсинг файлов, т.к с NodeJS я работаю очень редко. Нашёл такую штуку, как glob.

Погнали писать multiple entries.

const { basename, join, resolve } = require("path");const glob = require("glob");const componentFileRegEx = /\.(j|t)s(x)?$/;const sassFileRegEx = /\s[ac]ss$/;const getComponentsEntries = (pattern) => {  const entries = {};  glob.sync(pattern).forEach(file => {    const outFile = basename (file);    const entryName = outFile.replace(componentFileRegEx, "");    entries[entryName] = join(__dirname, file);  })  return entries;}module.exports = {  entry: getComponentsEntries("./components/**/*.tsx"),  output: {    filename: "[name].js",    path: resolve(__dirname, "build")  },  module: {    rules: [      {        test: componentFileRegEx,        loader: "babel-loader",        exclude: /node_modules/      },      {        test: sassFileRegEx,        use: ["style-loader", "css-loader", "sass-loader"]      }    ]  }  resolve: {    extensions: [".js", ".ts", ".tsx", ".jsx"],    alias: {      components: resolve(__dirname, "components")    }  }}

Готово. Собираем.

После сборки в каталог build упало 2 файла Button.js, Dropdown.js заглядываем внутрь. Внутри лицензии react.production.min.js, тяжелочитаемый минимизированный код, и куча всякой фигни. Окей, попробуем использовать кнопку.

В демо файле кнопки меняем импорт на импорт из каталога build.

Вот так выглядит простая демка кнопки в styleguidist Button.md

```javascriptimport Button from '../../build/Button'<Button>Кнопка</Button>```

Заходим посмотреть на кнопочку иии

Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.


На этом этапе уже отпали идея и желание собирать через webpack.

Ищем другой путь сборки без webpack


Идём за помощью к бабелю без вебпака. Пишем скрипт в package.json, указываем файл конфига, расширения, директорию где лежат компоненты, директорию куда собрать:

{  //...package.json всякие штуки-дрюки о которых обычно не паримся  scripts: {    "build": "babel --config-file ./.babelrc --extensions '.jsx, .tsx' ./components --out-dir ./build"  }}

запускаем:

npm run build

Вуаля, у нас в каталоге build появились 2 файла Button.js, Dropdown.js, внутри файлов красиво оформленный ванильный js + некоторые полифилы и одинокий requre(styles.scss). Явно это не сработает в демке, удаляем импорт стилей(в этот момент меня гложила надежда, что я найду плагин для транспила scss), собираем ещё раз.

После сборки у нас остался читсый JS. Повторяем попытку интеграции собранного компонента в styleguidist:

```javascriptimport Button from '../../build/Button'<Button>Кнопка</Button>```

Скомпилировалось работает. Только кнопочка без стилей.

Ищем плагин для транспила scss/sass


Да, сборка компонентов работает, компоненты работают, можно собирать, паблишить в npm или свой рабочий нехус(nexus). Ещё бы только стили сохранить Окей, снова гугл нам поможет (нет).

Гугления плагинов не принесли мне каких-то результатов. Один плагин генерирует строку из стилей, другой вообще не работает да ещё требует импорта вида:import styles from styles.scss (никогда так не импортировал стили и мое личное мнение не надо так делать, но о вкусах не спорят)

Единственная надежда была на этот плагин: babel-plugin-transform-scss-import-to-string, но он просто генерирует строку из стилей (а я уже говорил выше. Блин...). Дальше всё стало ещё хуже, я дошел до 6 страницы в гугле (а на часах уже 3 утра). Да и вариантов особо уже не будет что-то найти. Да и думать то нечего либо webpack + sass-loader, которые хреново это делают и не для моего случая, либо ШТО-ТО ДРУГОЕ. Нервы Я решил немного передохнуть, попить чай, спать всё равно не хочется. Пока делал чай, идея написать плагин для транспила scss/sass все больше и больше влетала в мою голову. Пока мешал сахарочек, редкие звоны ложки в моей голове отдавались эхом: Пиши плааагин. Ок, решено, буду писать плагин.

Плагин не найден. Пишем сами


За основу своего плагина я взял babel-plugin-transform-scss-import-to-string, упомянутый выше. Я прекрасно понимал, что сейчас будет геморрой с AST деревом, и прочими хитростями. Ладно, поехали.

Делаем предварительные подготовочки. Нам нужны node-sass и path, а так же регулярочки для файлов и расширений. Идея такая:

  • Получаем из строки импорта путь до файла со стилями
  • Парсим через node-sass стили в строку (спасибо babel-plugin-transform-scss-import-to-string)
  • Создаем style теги по каждому из импортов (плагин бабеля запускается на каждом импорте)
  • Надо как-то идентифицировать созданный стиль, что бы не накидывать одно и то же на каждый чих hot-reload. Впихнем ему какой-нибудь аттрибут (data-sass-component) со значением текущего файла и названием файла стилей. Будет что-то вроде этого:

          <style data-sass-component="Button_style">         .button {            display: flex;         }      </style>
    

В целях разработки плагина и тестирования на проекте, на уровне с каталогом components я создал babel-plugin-transform-scss каталог, запихнул туда package.json и запихнул туда каталог lib, а в него уже закинул index.js.
Что бы вы были вкурсе конфиг бабеля лезет за плагином, который указан в директиве main в package.json, для этого пришлось его запихать.
Указываем:

{  //...package.json опять всякие штуки-дрюки о которых обычно не паримся, да и кроме main ничего нету  main: "lib/index.js"}

Затем, пихаем в конфиг бабеля (.babelrc) путь до плагина:

{  //Тут всякие пресеты  plugins: [    "./babel-plugin-transform-scss"    //тут остальные плагины для сборки  ]}

А теперь напихиваем в index.js магию.

Первый этап проверка на импорт именно scss или sass файла, получение имени импортируемых файлов, получение имени самого js файла(компонента), транспил в css строку scss или sass. Подрубаемся через WebStorm к npm run build через дебаггер, ставим точки останова, смотрим аргументы path и state и выуживаем имена файлов, обрабатываем руглярочками:

const { resolve, dirname, join } = require("path");const { renderSync } = require("node-sass");const regexps = {  sassFile: /([A-Za-z0-9]+).s[ac]ss/g,  sassExt: /\.s[ac]ss$/,  currentFile: /([A-Za-z0-9]+).(t|j)s(x)/g,  currentFileExt: /.(t|j)s(x)/g};function transformScss(babel) {  const { types: t } = babel;  return {    name: "babel-plugin-transform-scss",    visitor: {      ImportDeclaration(path, state) {        /**         * Проверяем, содержит ли текущий файл scss/sass расширения в импорте         */        if (!regexps.sassExt.test(path.node.source.value)) return;        const sassFileNameMatch = path.node.source.value.match(          regexps.sassFile        );        /**         * Получаем имя текущего scss/sass файла и текущего js файла         */        const sassFileName = sassFileNameMatch[0].replace(regexps.sassExt, "");        const file = this.filename.match(regexps.currentFile);        const filename = `${file[0].replace(          regexps.currentFileExt,          ""        )}_${sassFileName}`;        /**         *         * Получаем полный путь до scss/sass файла, транспилим в строку css         */        const scssFileDirectory = resolve(dirname(state.file.opts.filename));        const fullScssFilePath = join(          scssFileDirectory,          path.node.source.value        );        const projectRoot = process.cwd();        const nodeModulesPath = join(projectRoot, "node_modules");        const sassDefaults = {          file: fullScssFilePath,          sourceMap: false,          includePaths: [nodeModulesPath, scssFileDirectory, projectRoot]        };        const sassResult = renderSync({ ...sassDefaults, ...state.opts });        const transpiledContent = sassResult.css.toString() || "";        }    }}

Fire. Первый успех, получена строка css в transpiledContent. Дальше самое страшное лезем в babeljs.io/docs/en/babel-types#api за API по AST дереву. Лезем в astexplorer.net пишем там код запихивания в head документа стилей.

В astexplorer.net пишем Self-Invoking функцию, которая будет вызываться на месте импорта стиля:

(function(){  const styles = "generated transpiledContent" // ".button {/n display: flex; /n}/n"   const fileName = "generated_attributeValue" //Button_style  const element = document.querySelector("style[data-sass-component='fileName']")  if(!element){    const styleBlock = document.createElement("style")    styleBlock.innerHTML = styles    styleBlock.setAttribute("data-sass-component", fileName)    document.head.appendChild(styleBlock)  }})()

В AST explorer тыкаем в левой части на строки, объявления, литералы, справа в дереве смотрим структуру объявлений, по этой структуре лезем в babeljs.io/docs/en/babel-types#api, курим всё это и пишем замену.

A few moments later

Спустя 1-1,5 часа, бегая по вкладкам из ast в babel-types api, затем в код, я написал замену импорта scss/sass. Разбирать отдельно дерево ast и babel-types api я не буду, будет ещё больше буковок. Показываю сразу результат:

const { resolve, dirname, join } = require("path");const { renderSync } = require("node-sass");const regexps = {  sassFile: /([A-Za-z0-9]+).s[ac]ss/g,  sassExt: /\.s[ac]ss$/,  currentFile: /([A-Za-z0-9]+).(t|j)s(x)/g,  currentFileExt: /.(t|j)s(x)/g};function transformScss(babel) {  const { types: t } = babel;  return {    name: "babel-plugin-transform-scss",    visitor: {      ImportDeclaration(path, state) {        /**         * Проверяем, содержит ли текущий файл scss/sass расширения в импорте         */        if (!regexps.sassExt.test(path.node.source.value)) return;        const sassFileNameMatch = path.node.source.value.match(          regexps.sassFile        );        /**         * Получаем имя текущего scss/sass файла и текущего js файла         */        const sassFileName = sassFileNameMatch[0].replace(regexps.sassExt, "");        const file = this.filename.match(regexps.currentFile);        const filename = `${file[0].replace(          regexps.currentFileExt,          ""        )}_${sassFileName}`;        /**         *         * Получаем полный путь до scss/sass файла, транспилим в строку css         */        const scssFileDirectory = resolve(dirname(state.file.opts.filename));        const fullScssFilePath = join(          scssFileDirectory,          path.node.source.value        );        const projectRoot = process.cwd();        const nodeModulesPath = join(projectRoot, "node_modules");        const sassDefaults = {          file: fullScssFilePath,          sourceMap: false,          includePaths: [nodeModulesPath, scssFileDirectory, projectRoot]        };        const sassResult = renderSync({ ...sassDefaults, ...state.opts });        const transpiledContent = sassResult.css.toString() || "";        /**         * Имплементируем функцию, написанную в AST Explorer и заменяем импорт методом          * replaceWith аргумента path.         */        path.replaceWith(          t.callExpression(            t.functionExpression(              t.identifier(""),              [],              t.blockStatement(                [                  t.variableDeclaration("const", [                    t.variableDeclarator(                      t.identifier("styles"),                      t.stringLiteral(transpiledContent)                    )                  ]),                  t.variableDeclaration("const", [                    t.variableDeclarator(                      t.identifier("fileName"),                      t.stringLiteral(filename)                    )                  ]),                  t.variableDeclaration("const", [                    t.variableDeclarator(                      t.identifier("element"),                      t.callExpression(                        t.memberExpression(                          t.identifier("document"),                          t.identifier("querySelector")                        ),                        [                          t.stringLiteral(                            `style[data-sass-component='${filename}']`                          )                        ]                      )                    )                  ]),                  t.ifStatement(                    t.unaryExpression("!", t.identifier("element"), true),                    t.blockStatement(                      [                        t.variableDeclaration("const", [                          t.variableDeclarator(                            t.identifier("styleBlock"),                            t.callExpression(                              t.memberExpression(                                t.identifier("document"),                                t.identifier("createElement")                              ),                              [t.stringLiteral("style")]                            )                          )                        ]),                        t.expressionStatement(                          t.assignmentExpression(                            "=",                            t.memberExpression(                              t.identifier("styleBlock"),                              t.identifier("innerHTML")                            ),                            t.identifier("styles")                          )                        ),                        t.expressionStatement(                          t.callExpression(                            t.memberExpression(                              t.identifier("styleBlock"),                              t.identifier("setAttribute")                            ),                            [                              t.stringLiteral("data-sass-component"),                              t.identifier("fileName")                            ]                          )                        ),                        t.expressionStatement(                          t.callExpression(                            t.memberExpression(                              t.memberExpression(                                t.identifier("document"),                                t.identifier("head"),                                false                              ),                              t.identifier("appendChild"),                              false                            ),                            [t.identifier("styleBlock")]                          )                        )                      ],                      []                    ),                    null                  )                ],                []              ),              false,              false            ),            []          )        );        }    }}

Итоговые радости


Ура!!! Импорт заменился на вызов функции, которая напихала в head документа стиль с этой кнопкой. И тут я подумал, а что если я стартану всю эту байдарку через вебпак, выкосив sass-loader? Будет ли оно работать? Окей, выкашиваем и проверяем. Запускаю сборку вебпаком, жду ошибку, что я должен определить loader для этого типа файла А ошибки-то нет, всё собралось. Открываю страницу, смотрю, а стиль воткнулся в head документа. Интересно получилось, я ещё избавился от 3 лоадеров для стилей(очень довольная улыбка).

Если вам была интересна статья поддержите звездочкой на github.

Так же ссылка на npm пакет: www.npmjs.com/package/babel-plugin-transform-scss

Примечание: Вне статьи добавлена проверка на импорт стиля по типу import styles from './styles.scss'
Источник: habr.com
К списку статей
Опубликовано: 03.08.2020 12:13:23
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Javascript

Node.js

Reactjs

Babel plugin

Babel

React.js

Ast

Babel-types

Webpack

Webpack 4

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru