14 września 2022 7 min. czytania

Konwertowanie tokenów projektowych na zmienne CSS z Node.js

Konwertowanie tokenów projektowych jest procesem podatnym na błędy - przekonałem się o tym na własnej skórze. Dlatego stworzyłem prosty skrypt dla środowiska Node.js, który pomoże mi z tym zadaniem.

Zaktualizowano: 14 września 2022
Zdjęcie zmiennych CSS w edytorze Visual Studio Code
Zdjęcie autorstwa Pankaj Patel

Ostatnio konwertowałem tokeny projektowe w języku JavaScript do zmiennych CSS. Miałem plik JS z różnymi aspektami strony umieszczonymi we właściwościach obiektu: rozmiary i rodzaje fontów, kolory itp. Wyglądało to podobnie do tego fragmentu kodu.

JS
1//tokens.js
2
3module.exports = {
4 font: {
5 family: {
6 heading: "'Source Sans Pro', sans-serif",
7 body: "'Roboto', sans-serif"
8 },
9 weight: {
10 normal: '400',
11 semibold: '500'
12 }
13 },
14 color: {
15 background: '#ffffff',
16 primary: {
17 light: '#4266b3',
18 base: '#16233f',
19 dark: '#06080f'
20 }
21 }
22 //more tokens
23}

Chciałem mieć zmienne CSS, więc przekopiowałem wszystkie właściwości z pliku JS do CSS. Taki był tego efekt.

CSS
1/* tokens.css */
2
3:root {
4 --font-family-heading: "'Source Sans Pro', sans-serif";
5 --font-family-body: "'Roboto', sans-serif";
6 --font-weight-normal: '400';
7 --font-weight-semibold: '500';
8 --color-background: '#ffffff';
9 --color-primary-light: '#4266b3';
10 --color-primary-default: '#16233f';
11 --color-primary-dark: '#06080f';
12 /* more variables */
13}

Nagle, przeniosłem się w czasie do lat 90. Moja strona wyglądała jakby zrobił ją Tim Berners-Lee we własnej osobie. Nie było żadnych stylów. Coś z układem było nie tak. Wprowadziłem także inne zmiany i pomyślałem, że problem leży w fazie budowania albo w styled-components. Debugowałem ten problem przez godzinę. . . żeby odkryć zbędne, podwójne cudzysłowy w pliku CSS. Tak, styled-components nie zadziałają z niepoprawnymi zmiennymi. Przestanę się zawstydzać publicznie i użyję tego błędu, aby nauczyć się czegoś publicznie.

Cel

Chcę zautomatyzować ten proces konwersji tokenów JS na zmienne CSS. Skrypt będzie przyjmował plik JavaScript z zagnieżdżonymi właściwościami motywu i zwróci plik CSS z poprawnie nazwanymi zmiennymi CSS. Chcę osiągnąć coś jak na przykładach powyżej. Ale bez nadmiarowych podwójnych cudzysłowów. Wiem, że prawdopodobnie istnieje gdzieś taki parser, ale chcę go napisać od zera i pouczyć się node.js. A jeżeli nie ma - poczuję się lepiej nie wymyślając koła na nowo. Możesz uczyć się ze mną. Trochę wiedzy o języku JavaScript i CSS przyda się, aby kontynuować.

Środowisko Node

Ja już mam zainstalowany Node.js na mojej maszynie. Jeżeli nie posiadasz tego środowiska uruchomieniowego, tu jest link do pobrania. Możesz użyć także nvm, aby zarządzać wieloma wersjami środowiska node. W tym projekcie użyję najnowszą wersję node - 18.3.0. Możesz sprawdzić swoją wersję node wpisując node -v lub nvm ls.

Mając node'a zainstalowanego, zainicjujmy nowy projekt z npm init. Prawdopodobnie nie użyjemy żadnej zewnętrznej paczki, ale inicjacja nowej nie zaszkodzi. Po przedarciu się przez proces konfiguracji, powinniśmy dostać nowy plik package.json.

Skrypt do konwertowania tokenów projektowych

Ze skonfigurowanym środowiskiem możemy utworzyć nasz pierwszy plik - index.js. Jeżeli wszystko działa poprawnie, ten fragment kodu powinien wypisać “hello” w terminalu.

JS
1console.log('Hello')

Na początek, zaimportujmy moduły, których użyjemy. Wykorzystam składnię require() dla dynamicznych importów. Ale możesz także użyć standardowych modułów ES - w tym celu musisz dodać pole "type": "module" do pliku package.json.

JS
1//index.js
2
3const { argv } = require('node:process')
4const { parse, format, normalize } = require('node:path')
5const { writeFile } = require('node:fs/promises')

Później musimy także zaimportować tokeny z pliku JS. Możemy ponownie wykorzystać require, ale tym razem z lokalną ścieżką do stylów podaną jako argument.

JS
1const tokens = require('./tokens.js')

Mając zaimportowany obiekt ze stylami, pora na trudniejszą część - konwertowanie ich do zmiennych CSS. Wszystkie style są w (zagnieżdżonych) obiektach. Na przykład standardowy rozmiar fontu jest zagnieżdżony kolejno w: font, size i body. Chcemy przetransformować go do zmiennej, która wygląda tak: --font-size-body: 1.5rem. Pomyślmy co musimy zrobić. Musimy konkatenować klucze z obiektów myślnikami, a gdy już nie ma więcej zagnieżdżonych obiektów, musimy dodać wartość do naszej świeżo przygotowanej zmiennej CSS.

JS
1const tokensToCss = (object, base = `-`) =>
2 Object.entries(object).reduce((css, [key, value]) => {
3 let newBase = base + `-${key}`
4 if (typeof value !== 'object') {
5 return css + newBase + `: ${value};\n`
6 }
7 return css + tokensToCss(value, newBase)
8 }, ``)

Ten krótki fragment kodu może być trudny do zrozumienia, więc postaraj się za mną podążać. Utworzyliśmy funkcję parsującą z dwoma parametrami: obiektem do sparsowania i aktualną podstawą. Wykorzystując metodę Object.entries(), zwracamy pary klucz-wartość wewnątrz tablicy. Na zwróconej tablicy wywołujemy metodę reduce(). Metoda ta przyjmuje dwa parametry: funkcję callback, która będzie wywołana dla każdego elementu tablicy i wartość początkową - pusty string. Wymieniony callback sam przyjmuje dwa parametry: poprzednią wartość, gdzie będziemy trzymać zakumulowane zmienne i aktualną wartość - tablicę (zdestrukturyzowaną do klucza i wartości). Wewnątrz funkcji callback, natychmiast tworzymy nową podstawę. To jest stara podstawa połączona z myślnikiem i aktualnym kluczem. Zmienne CSS definiujemy dwoma myślnikami, więc podstawa jest myślnikiem w domyśle. Zawsze chcemy konkatenować klucz w ten sposób. Zastanowić trzeba się nad wartością. Istnieją tylko dwie możliwości: wartość może być kolejnym zagnieżdżonym obiektem, albo prymitywną wartością. Jeżeli wartość jest obiektem, go również chcemy parsować. Dlatego w takim przypadku, zwracamy nasz akumulator plus wynik funkcji wywołującej samą siebie. Jednakże, tym razem, funkcji podajemy wymienioną wcześniej wartość. Zagnieżdżony obiekt może mieć wiele właściwości, dlatego funkcja parsująca potrzebuje nowej podstawy, aby zaaplikować ją do nich wszystkich. Jeżeli nie ma już więcej zagnieżdżonych obiektów, chcemy zakończyć naszą zmienną CSS wartością, średnikiem i nową linią. Wynikiem jest lista zmiennych - jedna pod drugą - utworzonych z podanego obiektu.

JS
1const { name } = parse('./tokens.js')
2const cssVariables = tokensToCss(tokens)
3const cssClass = `:root {\n${cssVariables.replaceAll('--', ' --')}}\n`
4
5writeFile(`${name}.css`, cssClass)

Zmienne CSS powinny być w jakiejś klasie, dlatego umieściłem je w pseudo-klasie :root, aby były globalnie dostępne. Dodałem także nowe linie i spacje, aby ją sformatować. Zapisałem klasę do pliku CSS używając metody writeFile(). Pierwszym argumentem, który przyjmuje jest nazwa oryginalnego pliku JS, ale z rozszerzeniem .css. Drugim argumentem jest nasz, przygotowany wcześniej string. Poniżej jest zawartość tego pliku.

CSS
1:root {
2 --font-family-heading: 'Source Sans Pro', sans-serif;
3 --font-family-body: 'Roboto', sans-serif;
4 --font-weight-normal: 400;
5 --font-weight-semibold: 500;
6 --color-background: #ffffff;
7 --color-primary-light: #4266b3;
8 --color-primary-default: #16233f;
9 --color-primary-dark: #06080f;
10}

Nasz skrypt działa, ale ścieżka jest zapisana na stałe. Zastąpiłem ją zmienną. Następnie użyłem właściwości argv, aby pobrać argumenty z wiersza poleceń. Podzieliłem je, ponieważ pierwszym argumentem jest ścieżka do komendy node, a drugim ścieżka do uruchomionego pliku. Nas interesują kolejne argumenty od użytkownika. Sformatowałem podany argument, ponieważ metoda require() wymaga początkowego ukośnika dla lokalnych plików. W ten sposób, nie ma znaczenia czy użytkownik poda nazwę skryptu tokens.js czy relatywną ścieżkę ./tokens.js. Na końcu zdestrukturyzowałem nazwę z oryginalnej ścieżki i użyłem jej w nowym pliku CSS.

JS
1const args = argv.slice(2)
2const tokensPath = format({ root: './', base: normalize(args[0]) })
3const tokens = require(tokensPath)
4const { name } = parse(tokensPath)

Teraz skrypt można wywoływać w taki sposób: node index.js tokens.js.

Ostateczny skrypt

Zrobiłem małą refaktoryzację ostatecznego skryptu. Dodałem podstawową obsługę błędów z niestandardowymi wiadomościami. Wydobyłem logikę odpowiedzialną za zapisywanie pliku i umieściłem ją w asynchronicznej funkcji, ponieważ metoda writeFile() w naszym przykładzie bazuje na obietnicach.

JS
1const { argv } = require('node:process')
2const { parse, format, normalize } = require('node:path')
3const { writeFile } = require('node:fs/promises')
4
5const tokensToCss = (object = {}, base = `-`) =>
6 Object.entries(object).reduce((css, [key, value]) => {
7 let newBase = base + `-${key}`
8 if (typeof value !== 'object') {
9 return css + newBase + `: ${value};\n`
10 }
11 return css + tokensToCss(value, newBase)
12 }, ``)
13
14const saveTokens = async (name, tokens) => {
15 try {
16 await writeFile(`${name}.css`, tokens)
17 } catch (e) {
18 console.log('There was an error while saving a file.\n', e)
19 }
20}
21
22try {
23 const args = argv.slice(2)
24 const tokensPath = format({ root: './', base: normalize(args[0]) })
25 const tokens = require(tokensPath)
26 const { name } = parse(tokensPath)
27
28 const cssVariables = tokensToCss(tokens)
29 const cssClass = `:root {\n${cssVariables.replaceAll('--', ' --')}}\n`
30 saveTokens(name, cssClass)
31} catch (e) {
32 console.log(
33 'Provide a correct argument - a relative path to design tokens.\n',
34 e
35 )
36}

Newsletter, który rozpala ciekawość💡

Subskrybuj mój newsletter, aby otrzymywać comiesięczną dawkę:

  • Nowości, przykładów, inspiracji ze świata front-end, programowania i designu
  • Teorii naukowych i sceptycyzmu
  • Moich ulubionych źródeł, idei, narzędzi i innych interesujących linków
Nie jestem nigeryjskim księciem, aby oferować ci okazje. Nie wysyłam spamu. Anuluj kiedy chcesz.

Pozostań ciekawy. Przeczytaj więcej

Babeczki z turkusowym lukrem udekorowane posypką21 października 20239 min. czytania

TypeScript - dekoratory

Udekorujmy nasz tor... kod używając dekoratorów TypeScript. Dekoratory są smacznym dodatkiem do klas i zapewniają składnię do metaprogramowania.

Czytaj wpis
Zużyty, czarno-biały klaps filmowy14 czerwca 20238 min. czytania

Podejmij akcję i naucz się GitHub Actions!

Programiści uwielbiają automatyzować rzeczy. Ale automatyzacja jest korzystna tylko wtedy, gdy zabiera mniej czasu niż zwraca. Dzięki GitHub Actions może osiągniemy ten zysk.

Czytaj wpis
Joker na szczycie rozsypanych kart19 kwietnia 20239 min. czytania

TypeScript - typy generyczne

Typy generyczne nie istnieją w JavaScripcie, ale są jednym z podstawowych konceptów w TypeScripcie. Oferują to co najlepsze z obu światów: elastyczność i bezpieczeństwo typów.

Czytaj wpis