
Spis treści

Jest taki rodzaj newsletterów, po których przeczytaniu człowiek ma ochotę rzucić wszystko i coś sprawdzić. Dziś trafiło na newsletter Kuby Mrugalskiego, a konkretnie na jedną pozycję – TinyJS, czyli framework do małych aplikacji desktopowych pisanych w JavaScripcie.
Od razu w głowie zapaliła mi się lampka. W poprzednim wpisie pisałem o Florino – SaaS-ie dla kwiaciarni i dekoratorów, który zbudowałem z Claude Code. Florino ma panel administracyjny w przeglądarce i API, które ten panel obsługuje. A skoro jest API, to czemu by nie spróbować zrobić do niego natywnej aplikacji na Maca? Nie „strony w okienku”, tylko prawdziwej aplikacji – z własnym interfejsem, menu, skrótami klawiszowymi, sesją trzymaną w systemowym pęku kluczy i ikoną w Docku.
Założenia na start były proste. Florino.app ma pozwolić zalogować się na swoje konto i obsłużyć 3 podstawowe widoki:
- dashboard – najważniejsze liczby i ostatnie wyceny
- listę wycen – razem z pełną edycją każdej z nich
- katalog usług – też z edycją
Spoiler: udało się. Wieczorem zbudowałem lokalnie aplikację, która się uruchamia i działa zgodnie z założeniami. A w obecnej formie – z własnymi ekranami wszystkich widoków – waży poniżej 10 MB. Jak do tego doszło – o tym jest ten wpis. No to lecimy…
Aplikacja natywna w JavaScripcie? Przecież to już było…
Było, i to nie raz. Pomysł, żeby pisać aplikacje desktopowe technologiami webowymi, ma już swoje lata, a frameworków do tego wyrosło jak grzybów po deszczu:
- Electron – król tej kategorii, na nim stoją m.in. VS Code, Slack czy Discord. Każda aplikacja wozi ze sobą własnego Chromium i własnego Node’a
- NW.js – starszy kuzyn Electrona, podobna idea i podobna waga
- Tauri – korzysta z systemowego WebView, więc jest dużo lżejszy, ale backend pisze się w Ruście
- Wails – to samo podejście, tylko backend w Go
- Neutralinojs – lekki, z systemowym WebView, ale z dość ograniczonym API
Kto robił kiedyś coś w Electronie, ten wie, że „Hello World” potrafi ważyć tyle, co niejedna gra sprzed 20 lat – typowa aplikacja to ok. 150 MB. Wygodne, ale ciężkie. Tauri czy Wails rozwiązują problem wagi, ale za cenę drugiego języka po stronie backendu – a front-endowiec niekoniecznie ma ochotę uczyć się Rusta tylko po to, żeby zapisać token w pęku kluczy.
TinyJS – a co to za zwierz?
I tu wchodzi TinyJS, który łączy oba światy:
- interfejs renderuje systemowy WebKit – ten sam, który napędza Safari, więc nie trzeba go dołączać do paczki
- backend to nadal JavaScript, tyle że uruchamiany w txiki.js – małym runtime JS zamiast pełnego Node’a
- komunikacja między oknem a backendem idzie przez gniazdo Unix – bez serwera HTTP i bez otwierania portów
Efekt? Wszystko zostaje w JavaScripcie, w środku mogę używać tego, co znam na co dzień – w moim przypadku Vue 3 i TypeScriptu – a gotowa aplikacja zajmuje na dysku tyle, co kilka zdjęć z telefonu. Moja Florino.app, z logowaniem, dashboardem, pełną edycją wycen i katalogiem, waży poniżej 10 MB. Dla porównania – sam Electron w wersji „Hello World” jest kilkanaście razy większy.
I to jest moim zdaniem największy wyróżnik TinyJS. Frameworków do aplikacji natywnych z webowym interfejsem jest sporo, ale mało który pozwala zachować jeden język w całym projekcie i zmieścić gotową aplikację w kilku MB.
Kto tu właściwie programował?
Uczciwie powiem od razu: kodu nie napisałem ani linijki. Całość stworzył Claude w trybie Cowork, a ja byłem w tym procesie tym, kim byłem przy samym Florino – określiłem wymagania i narzędzia, odpowiadałem na pytania, podejmowałem decyzje i testowałem kolejne etapy na Macu, aż do zbudowania gotowej aplikacji.
Zaczęło się koło 17:45 od prośby: przeanalizuj TinyJS, przejrzyj kod Florino i zaplanuj aplikację. Po kilku minutach dostałem plan (w pliku PLAN.md) i 4 pytania doprecyzowujące. Moje odpowiedzi ustawiły cały dalszy kierunek:
- Logowanie – zamiast podrabiać ciasteczka przeglądarki, robimy małą, czystą zmianę po stronie serwera Florino: plugin
bearerw Better Auth. Aplikacja dostaje token i wysyła go w nagłówkuAuthorization. - Frontend – Vue 3 + TypeScript, czyli ten sam ekosystem co Florino.
- Kliknięcie w wycenę otwiera ją w aplikacji, a nie w przeglądarce. Firma ma móc w pełni obsłużyć wyceny bez wychodzenia z Florino.app.
- Zakres – tylko produkcja (florino.io), logowanie na swoją firmę, bez przełączania między firmami.
Przy okazji wyszedł pierwszy ciekawy problem. Panel webowy loguje się ciasteczkiem domeny .florino.io – którego natywna aplikacja po prostu nie ma. Stąd decyzja o tokenie Bearer. Zanim jednak zlecono jakąkolwiek zmianę, Claude przeczytał kod źródłowy Better Auth i sprawdził, czy ochrona CSRF (sprawdzanie nagłówka origin) nie zablokuje logowania bez ciasteczek. Nie zablokuje – co potem zostało potwierdzone testem.
Dwóch agentów, jeden kontrakt
I tu zaczyna się część, która mnie samego najbardziej zaskoczyła. Zmiana w Florino to inne repozytorium, inny projekt, inne zasady. Zamiast mieszać, Claude przygotował samodzielny „wsad” – dokument z zakresem zmiany, listą rzeczy, których nie robić, wymaganymi testami i kryteriami akceptacji. Ten wsad zleciłem Claude Code, który pracował bezpośrednio w projekcie Florino.
Po mniej więcej pół godzinie wróciła odpowiedź – „wsad zwrotny”. Zmiana wdrożona na produkcję (Florino 0.14.2), z testem E2E, a do tego dokładny kontrakt API, na którym dalej oparto aplikację. Cała zmiana po stronie serwera to jedna linia konfiguracji, bez ruszania endpointów.
Co więcej – Claude Code po stronie Florino wyłapał pułapkę, której nikt się nie spodziewał. Odpowiedź z logowania zawiera pole token, ale to jest niepodpisane ID sesji, które serwer… odrzuca. Prawdziwy token przychodzi wyłącznie w nagłówku set-auth-token. Gdyby nie kontrakt, ktoś straciłby na tym dobrą godzinę 🙂
W praktyce wyglądało to trochę jak współpraca dwóch zespołów w firmie – jeden robi aplikację, drugi backend, a pomiędzy nimi jest dokument, który obie strony traktują jak umowę. A ja w roli kogoś, kto ten dokument przenosi z biurka na biurko i pilnuje, żeby nikt nie wyszedł poza ustalenia. Tylko że „spotkanie” trwało 30 minut, a nie 2 tygodnie.
Etap po etapie
Dalej poszło już według rytmu, który bardzo szybko się wyklarował: etap → build → test na Macu → kolejny etap. Ten ostatni krok był niezbędny, bo – o czym za chwilę – Claude nie miał jak zobaczyć swojej aplikacji.
Mniej więcej tak to wyglądało w czasie:
- ~18:50 – szkielet aplikacji i połączenie z API. Pierwszy ekran pokazywał tylko diagnostykę „API odpowiada (401)”, ale to wystarczyło, żeby wiedzieć, że cała rura działa
- ~19:05 – logowanie: e-mail i hasło, reset hasła, token w Keychainie, automatyczne logowanie po restarcie, ekran „brak połączenia” (który nie wylogowuje) i potwierdzenie wylogowania natywnym oknem macOS
- ~19:11 – powłoka aplikacji: sidebar z przezroczystością w stylu macOS, natywne menu Plik / Edycja / Widok, skróty ⌘1/⌘2/⌘3, ⌘N, ⌘R, ⌘F, ⌘S, ⌘P i tryb ciemny
- ~19:18 – dashboard
- ~19:24 – lista wycen
- ~19:37 – szczegóły wyceny z pełną edycją – największy etap z całego dnia
- ~19:59 – nowa wycena
- ~20:14 – wykończenie i katalog usług
- ~20:28 – pierwszy commit: 102 pliki
Od pierwszego pytania do commita z kompletną aplikacją – ok. 4,5 godziny. Wliczając w to czekanie na Claude Code po stronie Florino.
Dashboard
Dashboard to 5 kafli z najważniejszymi liczbami (nowe zapytania z trendem z ostatnich 7 dni, wyceny w trakcie, zamówienia, łączna wartość i dostępne kredyty), roczny wykres wycen z dymkami, podział na statusy i lista ostatnich wycen. Czyli to samo, co w panelu webowym – tylko w oknie, które otwiera się z Docka.

Lista i edycja wycen
Lista wycen ma podział na aktywne i archiwum, wyszukiwanie po ID, filtry statusu, typu i dat (z gotowymi presetami), sortowanie, paginację i nawigację strzałkami. Filtry są zapamiętywane, więc po ponownym otwarciu aplikacji wszystko jest tak, jak było.

Szczegóły wyceny to już pełnoprawne narzędzie do pracy: zmiana statusu i nazwy, edycja cen, ilości i komentarzy przy każdej pozycji, dodawanie pozycji z katalogu albo własnych, notatki, dane klienta i wydarzenia, wysyłka e-mailem, historia wersji, archiwizacja, usuwanie, druk i PDF. Jak coś zmienię, na dole pojawia się pasek „Niezapisane zmiany”, a ⌘S zapisuje – dokładnie tak, jak w każdej innej aplikacji na Maca.

Katalog usług – czyli zmiana planów w trakcie
Katalogu w pierwotnym planie… nie było. Miał być tylko link „otwórz w przeglądarce”. Ale koło 20:00, mając już działające wyceny, stwierdziłem, że skoro idzie tak dobrze, to dorzucamy. Kwadrans później był gotowy: grupy, usługi z wariantami cenowymi i typami wydarzeń, typy wydarzeń z ikonami i zmiana kolejności przeciąganiem.

Przy okazji wykończenia doszły rzeczy, które sprawiają, że to faktycznie „czuje się” jak aplikacja na Maca: liczba nowych wycen na ikonie w Docku, systemowe powiadomienie „Nowe zapytanie ofertowe”, menu pod prawym przyciskiem na wierszu wyceny, odświeżanie danych po powrocie do okna i własna ikona.
Jak to działa pod spodem?
Dla tych, którzy lubią zajrzeć pod maskę – w kilku zdaniach.
Okno to systemowy WebKit z aplikacją Vue. Ale – i to jest kluczowe – strona sama nie łączy się z internetem. Każde zapytanie idzie do backendu TinyJS:
tiny.api.call('apiRequest', …)
Backend dokleja token wyjęty z Keychaina macOS i dopiero on woła https://florino.io/api/admin/*. Pilnuje przy tym listy dozwolonych ścieżek, rozpoznaje typowe błędy (wygasła sesja, zablokowane konto, brak sieci) i sam „wylogowuje” użytkownika, gdy token przestaje działać.
Co to daje? Po pierwsze – zero problemów z CORS, bo z punktu widzenia serwera to zwykły klient HTTP, a nie przeglądarka. Po drugie – token nie krąży po kodzie strony, tylko siedzi w systemowym pęku kluczy i jest używany wyłącznie w backendzie. Kto kiedyś trzymał tokeny w localStorage, ten wie, o czym mówię…
Kilka liczb na dzień commita: ok. 5,3 tys. linii własnego kodu (TS + Vue), 77 testów jednostkowych w Vitest, ok. 25 endpointów API Florino w użyciu i 1 zmiana po stronie serwera.
Problemy…
Żeby nie było tak różowo – po drodze było kilka „ciekawostek”.
Claude nie widział aplikacji, którą budował. Jego powłoka działała w linuksowej maszynie wirtualnej bez ekranu, a do tego florino.io było z niej niedostępne. Jak więc sprawdzić, czy aplikacja na Maca działa, nie mając Maca? Każdy etap przechodził typecheck, testy, build i bundlowanie backendu tak, jak robi to TinyJS. Do tego zrzuty ekranu w Chromium z podstawionym, atrapowym backendem. A ostatnim testem byłem ja – z aplikacją uruchomioną na Macu. Trochę jak praca zdalna z bardzo sumiennym kolegą, któremu nie działa udostępnianie ekranu…
Chromium nie ładuje modułów JS z file://, a TinyJS ładuje stronę właśnie w ten sposób. Do zrzutów ekranu trzeba było postawić lokalny serwer HTTP.
tinyjs new nie działa w niepustym katalogu – a w katalogu był już plan i wsady. Szablon projektu został więc odtworzony ręcznie na podstawie źródeł CLI TinyJS.
Zerwane połączenie z komputerem w środku etapu 6 – czyli tego największego. Praca poszła dalej na kopii w chmurze, a przed nadpisaniem plików na Macu Claude porównał sumy kontrolne, żeby nie skasować żadnej zmiany. Przyznam, że tego się nie spodziewałem – sam pewnie bym po prostu skopiował i miał nadzieję, że będzie dobrze…
API potrafi zaskoczyć. Parametr ?archived=false w liście wycen zwraca… archiwum. Winowajcą jest z.coerce.boolean("false"), który zwraca true – bo niepusty string to w JS prawda. Klasyka. Aplikacja wysyła ten parametr tylko jako true.
Do tego garść drobniejszych pułapek:
- skrót ⇧⌘Q, który miał być „Wyloguj”, w macOS wylogowuje… z całego systemu
printToPDFw TinyJS daje jedną długą stronę, więc drukowanie idzie przez natywne okno drukowania- TinyJS ma jedno menu kontekstowe na całą stronę – aplikacja podmienia je, gdy kursor najedzie na wiersz wyceny
- zapomniane
npm installpo dodaniu biblioteki ikon i błąd Vite „Failed to resolve import” - w Heroicons v2 nie ma ikony „liść”, której używa panel webowy
- w trybie ciemnym ciemne logo znikało na ciemnym tle, a biały tekst na jasnozielonym przycisku był nieczytelny – kolory słupków wykresu dobrano w końcu walidatorem kontrastu
4500 zł, ale12 450 zł– polska lokalizacja grupuje tysiące dopiero od 5 cyfr. Wygląda jak błąd, ale panel webowy robi to samo, więc zostało – a test to dokumentuje
Błędy w webie znalezione przy okazji
I tu ciekawostka. Przenoszenie funkcji z panelu webowego do aplikacji to świetny sposób na code review – bo trzeba dokładnie zrozumieć, jak coś działa. Przy okazji wyszły 3 błędy w samym panelu Florino:
- zarchiwizowana wycena nie pokazuje pozycji
- zmiana statusu kasuje niezapisane zmiany w cenach
- brak ostrzeżenia przy wyjściu z niezapisanymi zmianami
Aplikacja na Maca ich nie ma, a dla panelu webowego powstała osobna lista do poprawy. Czyli eksperyment, który miał być tylko zabawą, przy okazji poprawi właściwy produkt 🙂
Czy było warto?
Jak dla mnie – zdecydowanie tak. Po południu przeczytałem o nowym narzędziu, wieczorem miałem działającą aplikację natywną z logowaniem, dashboardem, pełną obsługą wycen i katalogiem. Nie prototyp z 2 ekranami, tylko coś, czego faktycznie da się używać na co dzień.
Na sam TinyJS patrzę z dużą sympatią. O ile nie zastąpi Electrona w dużych aplikacjach, o tyle do małych narzędzi – nakładek na własne API, paneli, „klientów” do SaaS-a – wydaje się strzałem w dziesiątkę. Poniżej 10 MB, systemowy WebKit, JavaScript na backendzie i Vue na froncie. Miał kilka ograniczeń (jedno menu kontekstowe, PDF jako jedna długa strona), ale żadne nie zablokowało pracy.
Ale chyba ciekawsza lekcja z tego dnia jest inna. Najwięcej dała nie sama szybkość pisania kodu, tylko proces: plan, pytania na starcie, wsad dla drugiego agenta z jasnym kontraktem, małe etapy i człowiek jako ostatni test po każdym z nich. Dokładnie to, o czym pisałem w poprzednim wpisie – rola programisty przesuwa się w stronę kogoś, kto wyznacza ramy i pilnuje, żeby z nich nie wychodzić.
Co dalej?
Na tym na pewno się nie skończy. Aplikacja będzie dalej rozwijana – docelowo chcę przenieść do niej wszystkie widoki z panelu administracyjnego Florino.io, tak żeby sekcja „Na florino.io” w sidebarze w końcu zniknęła.
Jest tylko jeden haczyk. Ostatni krok – opublikowanie aplikacji, czyli podpis Developer ID, notaryzacja przez Apple i instalator DMG – póki co nie jest możliwy, bo nie mam konta deweloperskiego w Apple. Czy Florino.app trafi kiedyś do App Store? Jeszcze nie wiem. Na razie działa u mnie lokalnie i to mi w zupełności wystarcza. TinyJS pozwala też budować aplikacje na Windowsa i Linuksa, ale tego nie testowałem.
A Wy – próbowaliście już TinyJS albo innych lżejszych alternatyw dla Electrona? Dajcie znać w komentarzach, chętnie porównam wrażenia 🙂
Komentarze
Ładowanie komentarzy…


