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'