Como abandonei o webpack e escrevi o plugin babel para transpile scss / sass

fundo



Numa noite de sábado, eu estava sentado procurando maneiras de construir um UI-Kit usando o webpack. Como uma demonstração do kit de interface do usuário, eu uso o styleguidst. Claro, o webpack é inteligente e reúne todos os arquivos que estão no diretório de trabalho em um pacote e tudo gira e gira a partir daí.



Eu criei um arquivo entry.js, importei todos os componentes de lá e depois exportei de lá. Parece que está tudo bem.



import Button from 'components/Button'
import Dropdown from 'components/Dropdown '

export {
  Button,
  Dropdown 
}


E depois de montar tudo isso, obtive o output.js no qual, como esperado, estava tudo - todos os componentes do heap em um arquivo. Aqui surgiu a questão:

Como posso coletar todos os botões, menus suspensos, etc. separadamente para importar em outros projetos?

Mas também quero fazer o upload para o npm como um pacote.



Hmm ... Vamos em ordem.



Múltiplas entradas



Claro, a primeira ideia que vem à mente é analisar todos os componentes do diretório de trabalho. Tive que pesquisar um pouco no Google sobre como analisar arquivos, porque raramente trabalho com o NodeJS. Encontrei algo como glob .



Nós dirigimos para escrever várias entradas.



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")
    }
  }
}


Feito. Nós coletamos.



Após a montagem, 2 arquivos Button.js, Dropdown.js caíram no diretório de construção - vamos dar uma olhada dentro. Dentro da licença está react.production.min.js, código reduzido de difícil leitura e um monte de besteira. Ok, vamos tentar usar o botão.



No arquivo demo do botão, altere a importação para importar do diretório de construção.



É assim que uma demonstração simples de um botão se parece no styleguidist - Button.md



```javascript
import Button from '../../build/Button'
<Button></Button>
```


Vamos olhar o botão IR ... Nessa fase, a ideia e a vontade de coletar pelo webpack já desapareceram.



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









Procurando outro caminho de construção sem webpack



Pedimos ajuda a uma babel sem webpack. Escrevemos um script em package.json, especificamos o arquivo de configuração, extensões, o diretório onde os componentes estão localizados, o diretório onde construir:



{
  //...package.json  -     
  scripts: {
    "build": "babel --config-file ./.babelrc --extensions '.jsx, .tsx' ./components --out-dir ./build"
  }
}


corre:



npm run build


Voila, temos 2 arquivos Button.js, Dropdown.js no diretório de compilação, dentro dos arquivos há um vanilla js lindamente projetado + alguns polyfills e um solitário requre ("styles.scss") . Obviamente, isso não funcionará na demonstração, exclua a importação de estilos (naquele momento, eu estava ansioso para encontrar um plugin para o transpile scss) e colete-o novamente.



Após a montagem, ainda temos alguns JS legais. Vamos tentar novamente integrar o componente montado no guia de estilo:



```javascript
import Button from '../../build/Button'
<Button></Button>
```


Compilado - funciona. Apenas um botão sem estilos.



Estamos procurando um plugin para transpile scss / sass



Sim, a montagem dos componentes funciona, os componentes estão funcionando, você pode construir, publicar no npm ou em seu próprio nexo de trabalho. Ainda assim, é só salvar os estilos ... Ok, o Google vai nos ajudar novamente (não).



Pesquisar os plug-ins não me deu nenhum resultado. Um plugin gera uma string a partir de estilos, o outro não funciona e até requer a importação da view: import styles from "styles.scss"



A única esperança era para este plugin: babel-plugin-transform-scss-import-to-string, mas só gera uma string de estilos (ah ... eu disse acima. Droga ...). Aí tudo piorou, cheguei à página 6 do Google (e o relógio já são 3 da manhã). E não haverá opções específicas para encontrar algo. Sim, e não há nada em que pensar - seja webpack + sass-loader, que é péssimo fazendo isso e não para o meu caso, ou ALGUMA OUTRA. Nervosismo ... Resolvi dar um tempo, tomar chá, ainda não quero dormir. Enquanto eu preparava o chá, a ideia de escrever um plugin para o transpilador scss / sass ficava cada vez mais na minha cabeça. Enquanto o açúcar era mexido, o som ocasional de uma colher na minha cabeça ecoava: "Escreva plaagin". Ok, decidi, vou escrever um plugin.



Plugin não encontrado. Nós nos escrevemos



Usei o babel-plugin-transform-scss-import-to-string mencionado acima como base para meu plugin . Eu entendi perfeitamente que agora haverá hemorróidas com uma árvore AST e outros truques. OK, vamos lá.



Fazemos os preparativos preliminares. Precisamos de node-sass e path, bem como linhas regulares para arquivos e extensões. A ideia é esta:



  • Pegamos o caminho para o arquivo com estilos da linha de importação
  • Analisar estilos para string via node-sass (graças a babel-plugin-transform-scss-import-to-string)
  • Nós criamos tags de estilo para cada uma das importações (o plugin babel é lançado em cada importação)
  • É necessário identificar de alguma forma o estilo criado, para não jogar a mesma coisa em todos os espirros de hot-reload. Vamos empurrar para algum atributo (data-sass-component) com o valor do arquivo atual e o nome da folha de estilo. Haverá algo assim:



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


Para desenvolver o plugin e testá-lo no projeto, no nível do diretório de componentes, criei um diretório babel-plugin-transform-scss, coloquei package.json lá e coloquei o diretório lib lá, e já coloquei index.js nele.

O que você seria vkurse - A configuração do Babel está atrás do plugin, que é especificado na diretiva principal em package.json, para isso eu tive que cram-lo.
Indicamos:



{
  //...package.json   -     ,    main  
  main: "lib/index.js"
}


Em seguida, insira o caminho do plug-in na configuração do babel (.babelrc):



{
  //  
  plugins: [
    "./babel-plugin-transform-scss"
    //    
  ]
}


Agora, vamos colocar um pouco de mágica no index.js.



A primeira etapa é verificar a importação do arquivo scss ou sass, obter o nome dos arquivos importados, obter o nome do próprio arquivo js (componente), transportar o scss ou string sass para o css. Cortamos o WebStorm para npm executar a compilação por meio de um depurador, definir pontos de interrupção, olhar para o caminho e os argumentos de estado e pescar os nomes de arquivo, processá-los com curses:



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() || "";
        }
    }
}


Fogo. Primeiro sucesso, peguei a linha css em transpiledContent. A seguir, a pior coisa - subimos para babeljs.io/docs/en/babel-types#api para a API na árvore AST. Subimos no astexplorer.net e escrevemos o código para inserir a folha de estilo na cabeça.



No astexplorer.net, escreva uma função de auto-invocação que será chamada no local da importação do estilo:



(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)
  }
})()


No explorador AST, pique do lado esquerdo nas linhas, declarações, literais, - à direita na árvore olhamos para a estrutura das declarações, subimos em babeljs.io/docs/en/babel-types#api usando esta estrutura , fumemos tudo isso e escrevemos uma substituição.



Alguns momentos depois ...



1-1,5 horas depois, percorrendo as guias da api ast à babel-types e, em seguida, no código, escrevi uma substituição para a importação scss / sass. Não vou analisar a ast tree e a API de tipos de babel separadamente, haverá ainda mais letras. Eu imediatamente mostro o resultado:



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
            ),
            []
          )
        );
        }
    }
}


Alegrias finais



Hooray !!! A importação foi substituída por uma chamada a uma função que enfiava o estilo com esse botão no cabeçalho do documento. E então pensei, e se eu começar todo esse caiaque pelo webpack, cortando o sass-loader? será que vai dar certo? Ok, vamos cortar e verificar. Lanço a montagem com um webpack, esperando um erro que devo definir um carregador para este tipo de arquivo ... Mas não tem erro, está tudo montado. Abro a página, vejo e o estilo fica preso no cabeçalho do documento. Aconteceu de forma interessante, eu também me livrei de 3 carregadores de estilo (sorriso muito feliz).



Se você estava interessado no artigo, por favor , apoie-o com um asterisco no github .



Também um link para o pacote npm: www.npmjs.com/package/babel-plugin-transform-scss



Nota: Fora do artigo, foi adicionada uma verificação para importação de estilo por tipoimportar estilos de './styles.scss'



All Articles