Przejdź do treści

TinyJS – natywna aplikacja w kilka godzin i poniżej 10 MB

Autor: Maciej Walczak
Spis treści
Grafika: laptop z ciemnym dashboardem aplikacji, obok fragment kodu i napis „Natywna aplikacja macOS w JavaScript – TinyJS”

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:

  1. Logowanie – zamiast podrabiać ciasteczka przeglądarki, robimy małą, czystą zmianę po stronie serwera Florino: plugin bearer w Better Auth. Aplikacja dostaje token i wysyła go w nagłówku Authorization.
  2. Frontend – Vue 3 + TypeScript, czyli ten sam ekosystem co Florino.
  3. 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.
  4. 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.

Dashboard aplikacji Florino na macOS w trybie ciemnym: 5 kafli z liczbami, wykres wycen w 2026 roku, podział aktywnych wycen na statusy i tabela ostatnich wycen
Dashboard – na koncie demo, w trybie ciemnym

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.

Lista wycen w aplikacji Florino: zakładki Aktywne i Archiwum, wyszukiwarka, filtry statusu, typu i dat z presetami Tydzień, Miesiąc, Rok oraz tabela wycen
Lista wycen z filtrami

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.

Szczegóły wyceny w aplikacji Florino: lista usług z polami ceny i ilości, komentarz dla klienta, suma, notatki prywatne, historia zmian oraz panele klienta, wydarzenia i widoku klienta
Edycja wyceny

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.

Katalog usług w aplikacji Florino: grupy Dekoracje i Usługi dodatkowe, usługi z opisami, typami wydarzeń i cenami wariantów Basic, Standard, Premium
Katalog usług – kolejność ustawia się 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
  • printToPDF w 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 install po 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ł, ale 12 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…

Dodaj komentarz

  • Artykuły

    Programista w czasach AI

    Czy AI zabierze nam robotę? Nie sądzę. Ale zmieni ją tak, że za kilka lat sami jej nie poznamy – i mam na to dowód w postaci 543 commitów…

    13 min czytania

  • Artykuły

    Raport DESI 2020 – za mało kobiet w IT!

    Wkurzyłem się. Zupełnie przypadkiem trafiłem na artykuł Polska branża IT ma się słabo. Brakuje specjalistów, pracuje bardzo mało kobiet. Dotyczy on raportu…

    4 min czytania

  • Artykuły · Vue.js

    VUEX – kilka przemyśleń po roku kodowania

    Co to jest Vue i Vuex? Jeśli nie wiesz co to jest Vue lub Vuex – to zapraszam najpierw do obejrzenia nagrania z mojej prezentacji…

    11 min czytania