Contexte
Un samedi soir, j'étais assis et je cherchais des moyens de créer un UI-Kit à l'aide de Webpack. J'utilise styleguidst comme démo UI-kit. Bien sûr, Webpack est intelligent et il rassemble tous les fichiers qui se trouvent dans le répertoire de travail dans un seul paquet et tout tourne et tourne à partir de là.
J'ai créé un fichier entry.js, y ai importé tous les composants, puis exporté à partir de là. Il semble que tout va bien.
import Button from 'components/Button'
import Dropdown from 'components/Dropdown '
export {
Button,
Dropdown
}
Et après avoir construit tout cela, j'ai obtenu output.js dans lequel, comme prévu, tout était - tous les composants du tas dans un seul fichier. Ici la question s'est posée:
Comment puis-je collecter tous les boutons, listes déroulantes, etc. séparément pour les importer dans d'autres projets?Mais je souhaite également le télécharger sur npm en tant que package.
Hmm ... Allons dans l'ordre.
Plusieurs entrées
Bien entendu, la première idée qui me vient à l'esprit est d'analyser tous les composants du répertoire de travail. J'ai dû un peu chercher sur Google pour analyser les fichiers, car je travaille rarement avec NodeJS. J'ai trouvé une chose telle que glob .
Nous avons conduit à écrire plusieurs entrées.
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")
}
}
}
Terminé. Nous collectons.
Après la construction, 2 fichiers Button.js, Dropdown.js sont tombés dans le répertoire de construction - regardons à l'intérieur. À l'intérieur de la licence se trouve react.production.min.js, un code minifié difficile à lire et beaucoup de conneries. D'accord, essayons d'utiliser le bouton.
Dans le fichier de démonstration du bouton, modifiez l'importation pour importer à partir du répertoire de construction.
Voici à quoi ressemble une simple démonstration d'un bouton dans styleguidist - Button.md
```javascript
import Button from '../../build/Button'
<Button></Button>
```
On va regarder le bouton IR ... A ce stade, l'idée et l'envie de collecter via webpack ont déjà disparu.
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
Recherche d'un autre chemin de compilation sans webpack
Nous allons chercher de l'aide à un babel sans webpack. Nous écrivons un script dans package.json, spécifions le fichier de configuration, les extensions, le répertoire où se trouvent les composants, le répertoire où construire:
{
//...package.json -
scripts: {
"build": "babel --config-file ./.babelrc --extensions '.jsx, .tsx' ./components --out-dir ./build"
}
}
courir:
npm run build
Voila, nous avons maintenant 2 fichiers Button.js, Dropdown.js dans le répertoire de construction, à l'intérieur des fichiers il y a un js vanilla bien conçu + des polyfills et un requre solitaire ("styles.scss") . Évidemment, cela ne fonctionnera pas dans la démo, supprimez l'importation de styles (à ce moment-là, je rongeais l'espoir de trouver un plugin pour le transpile scss), et le récupérez à nouveau.
Après l'assemblage, nous avons encore de jolis JS. Essayons à nouveau d'intégrer le composant assemblé dans le styleguidist:
```javascript
import Button from '../../build/Button'
<Button></Button>
```
Compilé - cela fonctionne. Seul un bouton sans styles.
Nous recherchons un plugin pour transpile scss / sass
Oui, l'assemblage des composants fonctionne, les composants fonctionnent, vous pouvez créer, publier dans npm ou dans votre propre nexus de travail. Pourtant, enregistrez simplement les styles ... Ok, Google nous aidera à nouveau (non).
Googler les plugins ne m'a apporté aucun résultat. Un plugin génère une chaîne à partir de styles, l'autre ne fonctionne pas du tout, et nécessite même l'importation de la vue: importer des styles depuis "styles.scss"
Le seul espoir était pour ce plugin: babel-plugin-transform-scss-import-to-string, mais il génère juste une chaîne de styles (ah ... je l'ai dit plus haut. Merde ...). Puis tout a empiré, j'ai atteint la page 6 de Google (et il est déjà 3 heures du matin). Et il n'y aura pas d'options particulières pour trouver quelque chose. Oui, et il n'y a rien à penser - ni webpack + sass-loader, qui est nul à le faire et pas pour mon cas, ou QUELQUE CHOSE D'AUTRE. Les nerfs ... J'ai décidé de faire une pause, de boire du thé, je n'ai toujours pas envie de dormir. Pendant que je préparais du thé, l'idée d'écrire un plugin pour le transpile scss / sass me revenait de plus en plus en tête. Tandis que le sucre remuait, le rare tintement d'une cuillère résonna dans ma tête: «Écrivez plaagin». Ok, décidé, je vais écrire un plugin.
Plugin introuvable. Nous nous écrivons
J'ai pris le babel-plugin-transform-scss-import-to-string mentionné ci-dessus comme base de mon plugin . J'ai parfaitement compris que maintenant il y aura des hémorroïdes avec un arbre AST, et d'autres astuces. Okay allons-y.
Nous faisons des préparatifs préliminaires. Nous avons besoin de node-sass et de path, ainsi que de lignes régulières pour les fichiers et les extensions. L'idée est la suivante:
- Nous obtenons le chemin d'accès au fichier avec les styles de la ligne d'importation
- Analyser les styles en chaîne via node-sass (grâce à babel-plugin-transform-scss-import-to-string)
- Nous créons des balises de style pour chacune des importations (le plugin babel est lancé à chaque import)
- Il est nécessaire d'identifier en quelque sorte le style créé, afin de ne pas lancer la même chose à chaque éternuement de rechargement à chaud. Introduisons un attribut (data-sass-component) avec la valeur du fichier actuel et le nom de la feuille de style. Il y aura quelque chose comme ça:
<style data-sass-component="Button_style"> .button { display: flex; } </style>
Afin de développer le plugin et de le tester sur le projet, au niveau du répertoire des composants, j'ai créé un répertoire babel-plugin-transform-scss, y fourré package.json et y ai fourré le répertoire lib, et j'y ai déjà jeté index.js.
Que seriez-vous vkurse - Babel config grimpe derrière le plugin, qui est spécifié dans la directive principale de package.json, pour cela, je devais le bourrer.Nous indiquons:
{
//...package.json - , main
main: "lib/index.js"
}
Ensuite, poussez le chemin vers le plugin dans la configuration babel (.babelrc):
{
//
plugins: [
"./babel-plugin-transform-scss"
//
]
}
Maintenant, mettons de la magie dans index.js.
La première étape est de vérifier l'importation du fichier scss ou sass, d'obtenir le nom des fichiers importés, d'obtenir le nom du fichier js (composant) lui-même, de transporter la chaîne scss ou sass en css. Nous coupons WebStorm à npm run build via un débogueur, définissons des points d'arrêt, regardons les arguments de chemin et d'état et récupérons les noms de fichiers, les traitons avec des malédictions:
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() || "";
}
}
}
Feu. Premier succès, j'ai obtenu la ligne css dans transpiledContent. Ensuite, la pire chose - nous montons dans babeljs.io/docs/en/babel-types#api pour l'API sur l'arborescence AST. Nous montons dans astexplorer.net et écrivons le code pour pousser la feuille de style dans la tête.
Dans astexplorer.net, nous écrivons une fonction auto-appelante qui sera appelée à la place de l'importation de style:
(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)
}
})()
Dans l'explorateur AST, piquez à gauche sur les lignes, les déclarations, les littéraux, - à droite dans l'arbre, nous regardons la structure des déclarations, nous montons dans babeljs.io/docs/en/babel-types#api en utilisant cette structure , fumons tout cela et écrivons un remplacement.
Quelques instants plus tard ...
1 à 1,5 heures plus tard, parcourant les onglets de l'API ast aux types babel, puis dans le code, j'ai écrit un remplacement pour l'importation scss / sass. Je n'analyserai pas l'arborescence ast et l'api de types babel séparément, il y aura encore plus de lettres. Je montre immédiatement le résultat:
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
),
[]
)
);
}
}
}
Joies finales
Hourra !!! L'importation a été remplacée par un appel à une fonction qui a entassé le style avec ce bouton dans l'en-tête du document. Et puis j'ai pensé, que se passerait-il si je commençais tout ce kayak à travers le webpack, en tondant le chargeur? Est-ce que ça marchera? D'accord, nous tondons et vérifions. Je lance l'assemblage avec un webpack, en attendant une erreur que je dois définir un chargeur pour ce type de fichier ... Mais il n'y a pas d'erreur, tout est assemblé. J'ouvre la page, regarde et le style est coincé dans la tête du document. Il s'est avéré intéressant, je me suis également débarrassé de 3 chargeurs de style (sourire très heureux).
Si vous étiez intéressé par l'article, veuillez le soutenir avec un astérisque sur github .
Egalement un lien vers le package npm: www.npmjs.com/package/babel-plugin-transform-scss
Remarque: En dehors de l'article, ajout d'une vérification pour l'importation du style par typeimporter des styles depuis './styles.scss'