Cómo abandoné el paquete web y escribí babel-plugin para scss / sass transpile

Antecedentes



Un sábado por la noche estaba sentado y buscando formas de construir un UI-Kit usando webpack. Utilizo styleguidst como demostración del kit de interfaz de usuario. Por supuesto, webpack es inteligente y reúne todos los archivos que están en el directorio de trabajo en un paquete y, a partir de ahí, todo gira y gira.



Creé un archivo entry.js, importé todos los componentes allí y luego los exporté desde allí. Parece que todo está bien.



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

export {
  Button,
  Dropdown 
}


Y después de ensamblar todo esto, obtuve output.js, en el que, como se esperaba, todo estaba: todos los componentes del montón en un archivo. Aquí surgió la pregunta:

¿Cómo puedo recopilar todos los botones, menús desplegables, etc. por separado, que se importarían en otros proyectos?

Pero también quiero subirlo a npm como paquete.



Hmm ... Vamos en orden.



Múltiples entradas



Por supuesto, la primera idea que me viene a la mente es analizar todos los componentes del directorio de trabajo. Tuve que buscar en Google un poco sobre el análisis de archivos, porque rara vez trabajo con NodeJS. Encontré algo así como glob .



Condujimos para escribir varias 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")
    }
  }
}


Hecho. Nosotros coleccionamos.



Después de la compilación, 2 archivos Button.js, Dropdown.js cayeron en el directorio de compilación; veamos adentro. Dentro de la licencia está react.production.min.js, código minificado difícil de leer y mucha mierda. Bien, intentemos usar el botón.



En el archivo de demostración del botón, cambie la importación para importar desde el directorio de compilación.



Así es como se ve una demostración simple de un botón en styleguidist - Button.md



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


Vamos a mirar el botón IR ... En esta etapa, la idea y las ganas de recolectar a través de webpack ya han desaparecido.



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









Buscando otra ruta de compilación sin paquete web



Vamos en busca de ayuda a un babel sin paquete web. Escribimos un script en package.json, especificamos el archivo de configuración, las extensiones, el directorio donde se encuentran los componentes, el directorio donde construir:



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


correr:



npm run build


Voila, ahora tenemos 2 archivos Button.js, Dropdown.js en el directorio de compilación, dentro de los archivos hay un js vainilla muy bien diseñado + algunos polyfills y un requre solitario ("styles.scss") . Obviamente, esto no funcionará en la demostración, elimine la importación de estilos (en ese momento estaba mordiendo la esperanza de encontrar un complemento para el transpile de scss) y recójalo nuevamente.



Después del montaje, todavía tenemos algunos buenos JS. Intentemos de nuevo integrar el componente ensamblado en la guía de estilo:



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


Compilado, funciona. Solo un botón sin estilos.



Estamos buscando un complemento para transpile scss / sass



Sí, el ensamblaje de componentes funciona, los componentes están funcionando, puedes construir, publicar en npm o tu propio nexo de trabajo. Aún así, solo guarde los estilos ... Ok, Google nos ayudará nuevamente (no).



Buscar en Google los complementos no me dio ningún resultado. Un complemento genera una cadena a partir de estilos, el otro no funciona en absoluto e incluso requiere importar la vista: importar estilos de "styles.scss"



La única esperanza era para este complemento: babel-plugin-transform-scss-import-to-string, pero solo genera una cadena de estilos (ah ... dije arriba. Maldita sea ...). Luego todo se puso peor, llegué a la página 6 en Google (y el reloj ya son las 3 de la mañana). Y no habrá opciones particulares para encontrar algo. Sí, y no hay nada en que pensar, ni webpack + sass-loader, que lo hacen mal y no para mi caso, o ALGO OTRO. Nervios ... Decidí hacer una pausa, tomar té, todavía no quiero dormir. Mientras preparaba el té, la idea de escribir un complemento para el transpile scss / sass seguía viniendo a mi cabeza cada vez más. Mientras el azúcar se removía, el sonido ocasional de una cuchara en mi cabeza hizo eco: "Escribe plaagin". Ok, decidido, escribiré un complemento.



Complemento no encontrado. Nos escribimos



Tomé el babel-plugin-transform-scss-import-to-string mencionado anteriormente como base para mi complemento . Entendí perfectamente que ahora habrá hemorroides con un árbol AST y otros trucos. De acuerdo, vámonos.



Hacemos preparativos preliminares. Necesitamos node-sass y path, así como líneas regulares para archivos y extensiones. La idea es esta:



  • Obtenemos la ruta al archivo con estilos de la línea de importación.
  • Analizar estilos en cadenas a través de node-sass (gracias a babel-plugin-transform-scss-import-to-string)
  • Creamos etiquetas de estilo para cada una de las importaciones (el complemento de babel se lanza en cada importación)
  • Es necesario identificar de alguna manera el estilo creado, para no arrojar lo mismo en cada estornudo de recarga en caliente. Empujemos algún atributo (data-sass-component) con el valor del archivo actual y el nombre de la hoja de estilo. Habrá algo como esto:



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


Para desarrollar el complemento y probarlo en el proyecto, al nivel del directorio de componentes, creé el directorio babel-plugin-transform-scss, rellené package.json allí y metí el directorio lib allí, y ya agregué index.js en él.

¿Qué sería vkurse? La configuración de Babel se coloca detrás del complemento, que se especifica en la directiva principal en package.json, para esto tuve que abarrotarlo.
Le indicamos:



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


Luego, inserte la ruta al complemento en la configuración de babel (.babelrc):



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


Ahora, metamos algo de magia en index.js.



La primera etapa es verificar la importación del archivo scss o sass, obtener el nombre de los archivos importados, obtener el nombre del archivo js (componente) en sí, transportar la cadena scss o sass a css. Cortamos WebStorm para ejecutar npm build a través de un depurador, establecemos puntos de interrupción, miramos la ruta y los argumentos del estado y pescamos los nombres de los archivos, los procesamos con maldiciones:



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


Fuego. Primer éxito, obtuve la línea css en transpiledContent. A continuación, lo peor: subimos a babeljs.io/docs/en/babel-types#api para la API por árbol AST Entramos en astexplorer.net y escribimos el código para introducir la hoja de estilo en la cabeza.



En astexplorer.net, escriba una función de autoinvocación que se llamará en el lugar de la importación del 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)
  }
})()


En el explorador AST, pinche en el lado izquierdo en líneas, declaraciones, literales, - a la derecha en el árbol miramos la estructura de declaraciones, subimos a babeljs.io/docs/en/babel-types#api usando esta estructura , fumamos todo esto y escribimos un reemplazo.



Unos momentos después ...



1-1.5 horas más tarde, corriendo a través de las pestañas de ast a babel-types api, luego en el código, escribí un reemplazo para la importación scss / sass. No analizaré el árbol ast y la api de tipos babel por separado, habrá incluso más letras. Enseguida muestro el 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
            ),
            []
          )
        );
        }
    }
}


Alegrías finales



¡Hurra! La importación fue reemplazada por una llamada a una función que abarrotó el estilo con este botón en el encabezado del documento. Y luego pensé, ¿qué pasa si comienzo todo este kayak a través del paquete web, cortando el sass-loader? ¿Funcionará? Bien, cortamos y comprobamos. Empiezo el ensamblaje con un paquete web, esperando un error que debo definir un cargador para este tipo de archivo ... Pero no hay error, todo está ensamblado. Abro la página, miro y el estilo se pega en el encabezado del documento. Resultó interesante, también me deshice de 3 cargadores de estilo (sonrisa muy feliz).



Si estaba interesado en el artículo, apóyelo con un asterisco en github .



También un enlace al paquete npm: www.npmjs.com/package/babel-plugin-transform-scss



Nota: Fuera del artículo, se agregó una verificación para importar estilo por tipoimportar estilos de './styles.scss'



All Articles