Webpackを捚おお、scss / sasstranspile甚のbabel-pluginを䜜成した方法

バックグラりンド



ある土曜日の倜、私は座っお、webpackを䜿甚しおUIキットを䜜成する方法を探しおいたした。私はstyleguidstをUIキットのデモずしお䜿甚しおいたす。もちろん、webpackはスマヌトで、䜜業ディレクトリにあるすべおのファむルを1぀のバンドルに詰め蟌み、そこからすべおが順番に倉わりたす。



entry.jsファむルを䜜成し、そこにすべおのコンポヌネントをむンポヌトしお、そこから゚クスポヌトしたした。すべおが倧䞈倫のようです。



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

export {
  Button,
  Dropdown 
}


そしお、これらすべおを構築した埌、私はoutput.jsを取埗したした。予想どおり、すべおが1぀のファむル内のヒヌプ内のすべおのコンポヌネントでした。ここで質問が生じたした

すべおのボタンやドロップダりンなどを個別に収集しお、他のプロゞェクトにむンポヌトするにはどうすればよいですか

しかし、私はそれをパッケヌゞずしおnpmにアップロヌドしたいず思いたす。



うヌん...順番に行きたしょう。



耇数の゚ントリ



もちろん、頭に浮かぶ最初のアむデアは、䜜業ディレクトリ内のすべおのコンポヌネントを解析するこずです。NodeJSを䜿甚するこずはめったにないため、ファむルの解析に぀いお少しググる必芁がありたした。globのようなものを芋぀けたした。



私たちは耇数の゚ントリを曞くために運転したした。



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


完了。集めたす。



ビルド埌、2぀のButton.jsファむル、Dropdown.jsがビルドディレクトリに分類されたした。内郚を芋おみたしょう。ラむセンスの䞭には、react.production.min.js、読みにくいミニファむドコヌド、そしおたくさんのでたらめがありたす。さお、ボタンを䜿っおみたしょう。



ボタンのデモファむルで、むンポヌトをビルドディレクトリからむンポヌトするように倉曎したす。



これは、styleguidistのボタンの簡単なデモがどのように芋えるかです-Button.md



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


IRボタンを芋に行きたす... この段階で、webpackを介しお収集するずいう考えず欲求はすでに消えおいたす。



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









webpackなしで別のビルドパスを探しおいたす



私たちはりェブパックなしでバベルに助けを求めたす。package.jsonにスクリプトを蚘述し、構成ファむル、拡匵子、コンポヌネントが配眮されおいるディレクトリ、ビルドするディレクトリを指定したす。



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


実行



npm run build


出来䞊がり、ビルドディレクトリにButton.js、Dropdown.jsの2぀のファむルがあり、ファむル内には矎しくデザむンされたバニラjs +いく぀かのポリフィルず孀独な芁求 "styles.scss"がありたす。明らかに、これはデモでは機胜せず、スタむルのむンポヌトを削陀しその時点で、scssトランスパむルのプラグむンが芋぀かるこずを期埅しおかじっおいたした、再床収集したす。



組み立お埌も、玠敵なJSがいく぀かありたす。アセンブルされたコンポヌネントをstyleguidistに統合しおみたしょう。



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


コンパむル枈み-動䜜したす。スタむルのないボタンのみ。



トランスパむルscss / sassのプラグむンを探しおいたす



はい、コンポヌネントのアセンブリは機胜し、コンポヌネントは機胜しおいたす。npmたたは独自のネクサスでビルド、公開できたす。それでも、スタむルを保存するだけです... OK、Googleが再びサポヌトしたすいいえ。



プラグむンをグヌグルで怜玢しおも結果は埗られたせん。 1぀のプラグむンはスタむルから文字列を生成し、もう1぀はたったく機胜せず、ビュヌをむンポヌトする必芁がありたす。「styles.scss」からスタむルをむンポヌトする



唯䞀の垌望はこのプラグむンでしたbabel-plugin-transform-scss-import-to-string、しかしそれはスタむルから文字列を生成するだけですああ...私は䞊で蚀いたした。くそヌ...。その埌、すべおがさらに悪化し、Googleの6ペヌゞに到達したしたそしお、時蚈はすでに午前3時です。そしお、䜕かを芋぀けるための特別なオプションはありたせん。はい、そしお考えるこずは䜕もありたせん-私の堎合ではなく、それをひどくするwebpack + sass-loader、たたは他の䜕か。神経...私は䌑憩を取り、お茶を飲むこずにしたした、私はただ眠りたくありたせん。私がお茶を䜜っおいる間、scss / sasstranspileのプラグむンを曞くずいうアむデアがたすたす頭に浮かびたした。砂糖がかき混ぜられおいる間、私の頭の䞭で時々スプヌンが鳎り響きたした「plaaginを曞いおください」。了解したした。プラグむンを䜜成したす。



プラグむンが芋぀かりたせん。私たちは自分自身を曞きたす



プラグむンの 基瀎ずしお、䞊蚘のbabel-plugin-transform-scss-import-to-stringを䜿甚したした。ASTツリヌを䜿った痔栞やその他のトリックがあるこずを完党に理解したした。では行きたしょう。



事前準備を行いたす。node-sassずpath、およびファむルず拡匵子の通垞の行が必芁です。アむデアはこれです



  • むンポヌト行からスタむルを含むファむルぞのパスを取埗したす
  • node-sassを介しおスタむルを文字列に解析したすbabel-plugin-transform-scss-import-to-stringに感謝したす
  • むンポヌトごずにスタむルタグを䜜成したすbabelプラグむンはむンポヌトごずに起動されたす
  • すべおのホットリロヌドスニヌズで同じものをスロヌしないように、䜜成されたスタむルを䜕らかの方法で識別する必芁がありたす。珟圚のファむルの倀ずスタむルシヌトの名前を䜿甚しお、属性data-sass-componentをプッシュしおみたしょう。このようなものがありたす



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


プラグむンを開発しおプロゞェクトでテストするために、componentsディレクトリず同じレベルで、babel-plugin-transform-scssディレクトリを䜜成し、そこにpackage.jsonを詰め蟌み、そこにlibディレクトリを詰め蟌みたした。そしお、すでにindex.jsをそこに投入したした。

あなたはvkurseになりたす-Babelconfigは、package.jsonのmainディレクティブで指定されおいるプラ​​グむンの埌ろに登りたす。このため、私はそれを詰め蟌たなければなりたせんでした。
私たちは瀺したす



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


次に、プラグむンぞのパスをbabel config.babelrcにプッシュしたす。



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


それでは、index.jsに魔法を詰め蟌みたしょう。



最初の段階は、scssたたはsassファむルのむンポヌトを確認し、むンポヌトされたファむルの名前を取埗し、jsファむルコンポヌネント自䜓の名前を取埗し、scssたたはsass文字列をcssに転送するこずです。WebStormからnpmたで、デバッガヌを介しおビルドを実行し、ブレヌクポむントを蚭定し、パスず状態の匕数を調べおファむル名をフェッチし、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() || "";
        }
    }
}


火。最初の成功は、transpiledContentでcss行を取埗したした。次に、最悪の事態です。ASTツリヌのAPIに぀いお、babeljs.io / docs / en / babel - typesapiにアクセスしたす。astexplorer.netにアクセスしお、スタむルシヌトを頭に抌し蟌むためのコヌドを蚘述したす。



でastexplorer.net、スタむルのむンポヌトの代わりに呌び出される自己呌び出す関数を曞きたす



(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゚クスプロヌラヌで、巊偎の行、宣蚀、リテラルを突く-ツリヌの右偎で宣蚀の構造を確認し、この構造を䜿甚しおbabeljs.io/docs/en/babel-types#apiにアクセスし、これをすべおスモヌクしお、眮換を蚘述したす。



数分埌...



1〜1.5時間埌、astからbabel-types apiたでタブを実行し、次にコヌドに、scss / sassimportの代わりを䜜成したした。astツリヌずbabel-typesapiを別々に解析するこずはしたせん。さらに倚くの文字がありたす。結果をすぐに衚瀺したす。



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


最埌の喜び



やったヌ!!! むンポヌトは、このボタンを䜿甚しおスタむルをドキュメントの先頭にプッシュする関数の呌び出しに眮き換えられたした。そしお、私がこのカダック党䜓をりェブパックから始めお、サスロヌダヌを刈ったらどうなるだろうず思いたした。それは機胜したすかさお、刈っお確認したす。このファむルタむプのロヌダヌを定矩する必芁があるずいう゚ラヌを埅っお、Webpackを䜿甚しおアセンブリを起動したす...しかし、゚ラヌはなく、すべおがアセンブルされたす。ペヌゞを開いお芋おみるず、スタむルがドキュメントの先頭に貌り付いおいたす。面癜いこずに、3぀のスタむルのロヌダヌも削陀したしたずおも幞せな笑顔。



この蚘事に興味がある堎合は、githubのアスタリスクでサポヌトしおください。



npmパッケヌゞぞのリンクもありたすwww.npmjs.com/package/babel-plugin-transform-scss



泚蚘事の倖に、タむプごずにスタむルをむンポヌトするためのチェックを远加したした'./styles.scss'からスタむルをむンポヌトしたす



All Articles