Wie ich das Webpack über Bord geworfen und das Babel-Plugin für scss / sass transpile geschrieben habe

Hintergrund



An einem Samstagabend saß ich und suchte nach Möglichkeiten, ein UI-Kit mit Webpack zu erstellen. Als UI-Kit-Demo verwende ich styleguidst. Natürlich ist Webpack intelligent und stopft alle Dateien, die sich im Arbeitsverzeichnis befinden, in ein Bundle, und von dort aus dreht sich alles.



Ich habe eine entry.js-Datei erstellt, alle Komponenten dort importiert und von dort exportiert. Es scheint alles in Ordnung zu sein.



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

export {
  Button,
  Dropdown 
}


Und nachdem ich das alles zusammengestellt hatte, bekam ich output.js, in der sich erwartungsgemäß alles befand - alle Komponenten im Heap in einer Datei. Hier stellte sich die Frage:

Wie kann ich alle Schaltflächen, Dropdowns usw. separat sammeln, um sie in andere Projekte zu importieren?

Ich möchte es aber auch als Paket auf npm hochladen.



Hmm ... Lass uns in Ordnung gehen.



Mehrfache Einträge



Die erste Idee, die mir in den Sinn kommt, ist natürlich, alle Komponenten im Arbeitsverzeichnis zu analysieren. Ich musste ein bisschen über das Parsen von Dateien googeln, weil ich selten mit NodeJS arbeite. Fand so etwas wie glob .



Wir fuhren, um mehrere Einträge zu schreiben.



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


Erledigt. Wir sammeln.



Nach dem Build, 2 Button.js-Dateien, fiel Dropdown.js in das Build-Verzeichnis - schauen wir mal rein. In der Lizenz befindet sich react.production.min.js, schwer lesbarer minimierter Code und viel Bullshit. Okay, lass uns versuchen, den Knopf zu benutzen.



Ändern Sie in der Demo-Datei der Schaltfläche den Import in den Import aus dem Build-Verzeichnis.



So sieht eine einfache Demo eines Buttons in styleguidist - Button.md aus



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


Wir schauen uns den IR-Knopf an ... Zu diesem Zeitpunkt sind die Idee und der Wunsch, über das Webpack zu sammeln, bereits verschwunden.



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









Suchen Sie nach einem anderen Build-Pfad ohne Webpack



Wir helfen einem Babel ohne Webpack. Wir schreiben ein Skript in package.json, geben die Konfigurationsdatei, die Erweiterungen, das Verzeichnis an, in dem sich die Komponenten befinden, das Verzeichnis, in dem erstellt werden soll:



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


Lauf:



npm run build


Voila, wir haben 2 Dateien Button.js, Dropdown.js im Build-Verzeichnis, in den Dateien gibt es eine wunderschön gestaltete Vanille js + einige Polyfills und eine einzige Anforderung ("styles.scss") . Offensichtlich wird dies in der Demo nicht funktionieren. Entfernen Sie den Import von Stilen (in diesem Moment nagte ich an der Hoffnung, dass ich ein Plug-In für das scss-Transpile finden würde) und sammeln Sie es erneut



Nach der Montage haben wir noch ein paar nette JS. Versuchen wir noch einmal, die zusammengesetzte Komponente in den Styleguidist zu integrieren:



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


Kompiliert - es funktioniert. Nur eine Schaltfläche ohne Stile.



Wir suchen ein Plugin für transpile scss / sass



Ja, die Montage der Komponenten funktioniert, die Komponenten funktionieren, Sie können in npm oder in Ihrem eigenen Arbeitszusammenhang erstellen, veröffentlichen. Speichern Sie trotzdem einfach die Stile ... Ok, Google wird uns wieder helfen (nein).



Das Googeln der Plugins hat mir keine Ergebnisse gebracht. Ein Plugin generiert eine Zeichenfolge aus Stilen, das andere funktioniert überhaupt nicht und erfordert sogar das Importieren der Ansicht: Importieren von Stilen aus "styles.scss"



Die einzige Hoffnung bestand für dieses Plugin: babel-plugin-transform-scss-import-to-string, aber es generiert nur eine Zeichenfolge aus Stilen (ah ... ich sagte oben. Verdammt ...). Dann wurde alles noch schlimmer, ich erreichte Seite 6 in Google (und die Uhr ist schon 3 Uhr morgens). Und es wird keine besonderen Möglichkeiten geben, etwas zu finden. Ja, und es gibt nichts zu denken - entweder Webpack + Sass-Loader, die es schlecht machen und nicht für meinen Fall, oder etwas anderes. Nerven ... Ich habe beschlossen, eine Pause zu machen, Tee zu trinken, ich will immer noch nicht schlafen. Während ich Tee kochte, kam mir immer mehr die Idee in den Sinn, ein Plugin für das scss / sass-Transpile zu schreiben. Während sich der Zucker rührte, hallte das gelegentliche Klingeln eines Löffels in meinem Kopf wider: "Schreibe Plaagin." Ok, beschlossen, ich werde ein Plugin schreiben.



Plugin nicht gefunden. Wir schreiben uns



Ich habe das oben erwähnte babel-plugin-transform-scss-import-to-string als Grundlage für mein Plugin genommen . Ich habe vollkommen verstanden, dass es jetzt Hämorrhoiden mit einem AST-Baum und anderen Tricks geben wird. Okay, dann los.



Wir treffen vorbereitende Vorbereitungen. Wir benötigen Node-Sass und Path sowie reguläre Punkte für Dateien und Erweiterungen. Die Idee ist folgende:



  • Wir erhalten den Pfad zur Datei mit Stilen aus der Importzeile
  • Analysieren Sie Stile, um sie über Node-Sass zu stringieren (dank babel-plugin-transform-scss-import-to-string).
  • Wir erstellen Style-Tags für jeden Import (das Babel-Plugin wird bei jedem Import gestartet).
  • Es ist notwendig, den erstellten Stil irgendwie zu identifizieren, um nicht bei jedem Niesen beim erneuten Laden dasselbe zu werfen. Lassen Sie uns ein Attribut (data-sass-component) mit dem Wert der aktuellen Datei und dem Namen des Stylesheets verschieben. Es wird so etwas geben:



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


Um das Plugin zu entwickeln und im Projekt zu testen, habe ich auf der Ebene des Komponentenverzeichnisses ein Verzeichnis babel-plugin-transform-scss erstellt, dort package.json gestopft und dort das lib-Verzeichnis gestopft, und ich habe bereits index.js hineingeworfen.

Was wären Sie vkurse - Babel config klettert hinter das Plugin, das in der Hauptanweisung in package.json angegeben ist, dafür musste ich es stopfen.
Wir geben an:



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


Schieben Sie dann den Pfad zum Plugin in die babel config (.babelrc):



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


Lassen Sie uns nun etwas Magie in index.js stopfen.



Der erste Schritt besteht darin, nach dem Import der scss- oder sass-Datei zu suchen, den Namen der importierten Dateien abzurufen, den Namen der js-Datei (Komponente) selbst abzurufen und die scss- oder sass-Zeichenfolge nach css zu transportieren. Wir durchschneiden WebStorm, um npm durch einen Debugger zu erstellen, setzen Haltepunkte, sehen uns die Pfad- und Statusargumente an, rufen die Dateinamen ab und verarbeiten sie mit Flüchen:



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


Feuer. Erster Erfolg, bekam die CSS-Zeile in transpiledContent. Als nächstes klettern wir in babeljs.io/docs/en/babel-types#api für die API im AST-Baum. Wir klettern in astexplorer.net und schreiben den Code, um das Stylesheet in den Kopf zu schieben .



In astexplorer.net schreiben wir eine selbstaufrufende Funktion, die an der Stelle des Stilimports aufgerufen wird:



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


Stöbern Sie im AST-Explorer auf der linken Seite in Zeilen, Deklarationen, Literalen. Rechts im Baum sehen wir uns die Struktur der Deklarationen an. Mit dieser Struktur steigen wir in babeljs.io/docs/en/babel-types#api auf , rauchen all dies und schreiben einen Ersatz.



Ein paar Momente später ...



1-1,5 Stunden später, als ich durch die Registerkarten von ast bis babel-types api lief und dann in den Code schrieb, schrieb ich einen Ersatz für den scss / sass-Import. Ich werde den Ast-Baum und die Babel-Typ-API nicht separat analysieren, es wird noch mehr Buchstaben geben. Ich zeige sofort das Ergebnis:



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


Letzte Freuden



Hurra!!! Der Import wurde durch einen Aufruf einer Funktion ersetzt, die den Stil mit dieser Schaltfläche in den Kopf des Dokuments drückte. Und dann dachte ich, was ist, wenn ich dieses ganze Kajak durch das Webpack starte und den Sass-Loader mähe? Wird es funktionieren? Okay, wir mähen und überprüfen. Ich starte die Assembly mit einem Webpack und warte auf einen Fehler, dass ich einen Loader für diesen Dateityp definieren muss ... Aber es gibt keinen Fehler, alles ist zusammengestellt. Ich öffne die Seite, schaue und der Stil steckt im Kopf des Dokuments. Es stellte sich interessanterweise heraus, dass ich auch 3 Style-Lader loswurde (sehr fröhliches Lächeln).



Wenn Sie an dem Artikel interessiert waren, unterstützen Sie ihn bitte mit einem Sternchen auf Github .



Außerdem ein Link zum npm-Paket: www.npmjs.com/package/babel-plugin-transform-scss



Hinweis: Außerhalb des Artikels wurde eine Prüfung zum Importieren des Stils nach Typ hinzugefügtStile aus './styles.scss' importieren



All Articles