jiplak 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wahyu Agus Arifin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.id.md ADDED
@@ -0,0 +1,312 @@
1
+ <div align="center">
2
+
3
+ # jiplak
4
+
5
+ **Skeleton loading otomatis, dijiplak dari tampilan asli aplikasi React kamu.**
6
+
7
+ Bungkus komponen. Jalankan satu perintah. Skeleton langsung sama persis.
8
+
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
10
+ [![React](https://img.shields.io/badge/React-%E2%89%A518-149eca?logo=react&logoColor=white)](https://react.dev)
11
+ [![TypeScript](https://img.shields.io/badge/TypeScript-ready-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
12
+ [![Sponsored by Lokio](https://img.shields.io/badge/sponsored%20by-Lokio-FF7A57)](https://lokio.link)
13
+
14
+ [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?style=for-the-badge&logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/wahyuagusarifin)
15
+
16
+ [English](./README.md) · Bahasa Indonesia
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ ## Kenapa jiplak?
23
+
24
+ Skeleton yang digambar manual cepat tidak sesuai lagi begitu layout berubah.
25
+ jiplak memotret komponen asli di browser dan menyimpan bentuknya. Kamu tidak
26
+ perlu menggambar skeleton lagi.
27
+
28
+ - **Tanpa menggambar.** Bentuk diambil dari DOM asli: teks, gambar, tombol.
29
+ - **Tanpa daftar route.** CLI menjelajah aplikasi lewat link-nya sendiri.
30
+ - **Responsif.** Dipotret di beberapa lebar layar, dipilih otomatis saat tampil.
31
+ - **Satu file YAML.** `jiplak.yaml`, cukup satu baris untuk mulai.
32
+ - **Mode gelap.** Mengikuti class `.dark` (konvensi Tailwind).
33
+ - **Runtime kecil.** Satu komponen React; alat pemotret tidak ikut ke browser.
34
+
35
+ ## Daftar isi
36
+
37
+ - [Mulai cepat](#mulai-cepat)
38
+ - [Pertanyaan umum](#pertanyaan-umum)
39
+ - [Konfigurasi](#konfigurasi)
40
+ - [API](#api)
41
+ - [Cara kerja](#cara-kerja)
42
+ - [Sponsor](#sponsor)
43
+ - [Pembuat](#pembuat)
44
+ - [Lisensi](#lisensi)
45
+
46
+ ## Mulai cepat
47
+
48
+ ### 1. Install
49
+
50
+ ```bash
51
+ npm install jiplak
52
+ # atau
53
+ bun add jiplak
54
+ ```
55
+
56
+ ### 2. Bungkus komponen yang menunggu data
57
+
58
+ ```tsx
59
+ import { Skeleton } from 'jiplak/react'
60
+
61
+ function Profil() {
62
+ const { data, isLoading } = useProfil()
63
+
64
+ return (
65
+ <Skeleton name="profil-card" loading={isLoading}>
66
+ <ProfilCard user={data} />
67
+ </Skeleton>
68
+ )
69
+ }
70
+ ```
71
+
72
+ ### 3. Buat `jiplak.yaml` di root project
73
+
74
+ ```yaml
75
+ url: http://localhost:3000
76
+ ```
77
+
78
+ ### 4. Jalankan aplikasi, lalu potret
79
+
80
+ ```bash
81
+ npm run dev
82
+ ```
83
+
84
+ Di terminal lain:
85
+
86
+ ```bash
87
+ npx jiplak build
88
+ ```
89
+
90
+ ### 5. Import registry hasil build sekali saja
91
+
92
+ Import dari komponen client yang selalu dimuat, misalnya providers:
93
+
94
+ ```tsx
95
+ 'use client'
96
+ import './bones/registry'
97
+ ```
98
+
99
+ Selesai. Saat `loading` bernilai `true`, skeleton tampil otomatis.
100
+
101
+ ## Pertanyaan umum
102
+
103
+ <details>
104
+ <summary><b>Apakah setiap screen harus diberi <code>&lt;Skeleton&gt;</code>?</b></summary>
105
+
106
+ Tidak. Bungkus hanya bagian yang menunggu data, misalnya card, list, atau
107
+ tabel. Bagian statis seperti header dan menu tidak perlu. Satu screen bisa
108
+ punya beberapa `<Skeleton>` dengan `name` yang berbeda.
109
+
110
+ </details>
111
+
112
+ <details>
113
+ <summary><b>Apakah skeleton benar-benar dibuat otomatis?</b></summary>
114
+
115
+ Ya. `jiplak build` menjelajah aplikasi lewat link, menemukan setiap
116
+ `<Skeleton>`, lalu mencatat bentuknya. Tugas kamu hanya membungkus komponen.
117
+
118
+ </details>
119
+
120
+ <details>
121
+ <summary><b>Kapan harus menjalankan <code>jiplak build</code> lagi?</b></summary>
122
+
123
+ Setelah layout komponen yang dibungkus berubah, atau setelah menambah
124
+ `<Skeleton>` baru. Commit folder `src/bones/` supaya tim tidak perlu build
125
+ ulang.
126
+
127
+ </details>
128
+
129
+ <details>
130
+ <summary><b>Bagaimana sebelum build pertama?</b></summary>
131
+
132
+ Aplikasi berjalan normal. Komponen tampil seperti biasa, hanya tanpa
133
+ skeleton.
134
+
135
+ </details>
136
+
137
+ <details>
138
+ <summary><b>Komponen saya kosong tanpa data.</b></summary>
139
+
140
+ Beri contoh isi lewat `fixture`. jiplak memotret `fixture`, bukan komponen
141
+ aslinya:
142
+
143
+ ```tsx
144
+ <Skeleton name="profil-card" loading={isLoading} fixture={<ProfilCard user={contohUser} />}>
145
+ <ProfilCard user={data} />
146
+ </Skeleton>
147
+ ```
148
+
149
+ </details>
150
+
151
+ <details>
152
+ <summary><b>Ada halaman yang tidak ketemu.</b></summary>
153
+
154
+ Penjelajah hanya menemukan halaman yang punya link dari halaman lain.
155
+ Tambahkan halaman tanpa link sebagai titik awal:
156
+
157
+ ```yaml
158
+ routes:
159
+ - /
160
+ - /pengaturan/profil
161
+ ```
162
+
163
+ </details>
164
+
165
+ <details>
166
+ <summary><b>Ada halaman yang tidak boleh dikunjungi.</b></summary>
167
+
168
+ Link logout dan file (pdf, gambar, csv) selalu dilewati. Untuk path lain:
169
+
170
+ ```yaml
171
+ ignore:
172
+ - /admin
173
+ ```
174
+
175
+ </details>
176
+
177
+ <details>
178
+ <summary><b>Build berhenti di <code>maxPages</code>.</b></summary>
179
+
180
+ Penjelajah berhenti di 50 halaman supaya route dinamis (`/post/1`,
181
+ `/post/2`, …) tidak dijelajah tanpa akhir. Naikkan bila perlu:
182
+
183
+ ```yaml
184
+ maxPages: 200
185
+ ```
186
+
187
+ </details>
188
+
189
+ <details>
190
+ <summary><b>Halaman saya butuh login.</b></summary>
191
+
192
+ Kirim cookie sesi:
193
+
194
+ ```yaml
195
+ cookies:
196
+ - session=isi-cookie-kamu
197
+ ```
198
+
199
+ </details>
200
+
201
+ <details>
202
+ <summary><b>Browser tidak ditemukan.</b></summary>
203
+
204
+ Pakai Chrome yang sudah terpasang:
205
+
206
+ ```yaml
207
+ channel: chrome
208
+ ```
209
+
210
+ Atau unduh Chromium sekali:
211
+
212
+ ```bash
213
+ npx playwright install chromium
214
+ ```
215
+
216
+ </details>
217
+
218
+ ## Konfigurasi
219
+
220
+ Semua key opsional. Nilai di bawah adalah default. Key yang salah ketik
221
+ langsung ditolak dengan pesan error.
222
+
223
+ ```yaml
224
+ # pemotretan
225
+ url: http://localhost:3000 # alamat aplikasi yang berjalan
226
+ routes: [/] # titik awal jelajah
227
+ ignore: [] # path yang tidak dikunjungi
228
+ maxPages: 50 # batas jumlah halaman
229
+ breakpoints: [375, 768, 1280] # lebar layar yang dipotret
230
+ out: ./src/bones # folder hasil
231
+ wait: 800 # jeda (ms) setelah halaman dimuat
232
+ channel: chrome # opsional: pakai Chrome yang terpasang
233
+ cookies: [] # opsional: name=value
234
+
235
+ # tampilan (props di <Skeleton> menimpa nilai ini)
236
+ animate: pulse # pulse | shimmer | solid
237
+ color: "#ececec" # mode terang
238
+ darkColor: "#262626" # mode gelap (class .dark)
239
+ shimmerColor: "#f7f7f7"
240
+ darkShimmerColor: "#323232"
241
+ speed: 1.6s
242
+ ```
243
+
244
+ ## API
245
+
246
+ ### `<Skeleton>`
247
+
248
+ | Prop | Wajib | Fungsi |
249
+ | --- | --- | --- |
250
+ | `name` | ya | Nama unik: huruf, angka, `-`, `_` |
251
+ | `loading` | ya | `true` saat data belum datang |
252
+ | `fixture` | tidak | Contoh isi yang dipotret saat build |
253
+ | `animate`, `color`, `darkColor`, `shimmerColor`, `darkShimmerColor`, `speed` | tidak | Menimpa `jiplak.yaml` untuk skeleton ini saja |
254
+ | `options` | tidak | `{ exclude: ['.badge'] }` melewati elemen; `{ leafTags: ['section'] }` menjadikan satu tag satu kotak |
255
+ | `bones` | tidak | Data skeleton langsung, tanpa registry |
256
+ | `className` | tidak | Class untuk elemen pembungkus |
257
+
258
+ ### Fungsi
259
+
260
+ | Fungsi | Kegunaan |
261
+ | --- | --- |
262
+ | `registerBones(map)` | Mendaftarkan bones per nama. Dipanggil oleh registry hasil build. |
263
+ | `configureJiplak(theme)` | Mengatur tampilan global. Dipanggil oleh registry dengan nilai dari YAML. |
264
+
265
+ ### CLI
266
+
267
+ ```bash
268
+ jiplak build [url] [--config jiplak.yaml]
269
+ ```
270
+
271
+ ## Cara kerja
272
+
273
+ 1. `jiplak build` membuka aplikasi di browser tanpa tampilan, mulai dari
274
+ `routes`, lalu mengikuti link di domain yang sama.
275
+ 2. Di setiap lebar layar, setiap `<Skeleton>` dipotret. Teks, gambar, dan
276
+ tombol dicatat sebagai kotak ("bones").
277
+ 3. Kotak disimpan ke `src/bones/<name>.bones.json`, ditambah `registry.ts`
278
+ yang memuatnya.
279
+ 4. Saat `loading`, kotak digambar di atas komponen yang disembunyikan,
280
+ memakai lebar layar yang paling cocok dengan lebar container.
281
+
282
+ ## Pengembangan
283
+
284
+ ```bash
285
+ bun install
286
+ bun test
287
+ bun run typecheck
288
+ bun run build
289
+ ```
290
+
291
+ Issue dan pull request terbuka di
292
+ [GitHub](https://github.com/ItPohgero/jiplak/issues).
293
+
294
+ ## Sponsor
295
+
296
+ <a href="https://lokio.link">
297
+ <img src="https://raw.githubusercontent.com/ItPohgero/jiplak/main/.github/assets/lokio.png" alt="Lokio" width="72" />
298
+ </a>
299
+
300
+ jiplak disponsori oleh **[Lokio](https://lokio.link)**.
301
+
302
+ Suka jiplak? Dukung pengembangannya:
303
+
304
+ [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?style=for-the-badge&logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/wahyuagusarifin)
305
+
306
+ ## Pembuat
307
+
308
+ **Wahyu Agus Arifin** · [@ItPohgero](https://github.com/ItPohgero)
309
+
310
+ ## Lisensi
311
+
312
+ [MIT](./LICENSE) © 2026 Wahyu Agus Arifin
package/README.md CHANGED
@@ -1,3 +1,313 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # jiplak
4
+
5
+ **Pixel-perfect skeleton loading screens, auto-captured from your real React UI.**
6
+
7
+ Wrap a component. Run one command. Get a skeleton that matches it exactly.
8
+
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
10
+ [![React](https://img.shields.io/badge/React-%E2%89%A518-149eca?logo=react&logoColor=white)](https://react.dev)
11
+ [![TypeScript](https://img.shields.io/badge/TypeScript-ready-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
12
+ [![Sponsored by Lokio](https://img.shields.io/badge/sponsored%20by-Lokio-FF7A57)](https://lokio.link)
13
+
14
+ [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?style=for-the-badge&logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/wahyuagusarifin)
15
+
16
+ English · [Bahasa Indonesia](./README.id.md)
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ ## Why jiplak?
23
+
24
+ Hand-drawn skeletons drift out of sync with the real UI the moment a layout
25
+ changes. jiplak ("to trace" in Indonesian) traces your rendered components in
26
+ a real browser and stores their exact shape. You never draw a skeleton again.
27
+
28
+ - **Zero drawing.** Bones come from the real DOM: text, images, buttons.
29
+ - **Zero route list.** The CLI crawls your app through its own links.
30
+ - **Responsive.** Captured at several screen widths; the right one is picked at runtime.
31
+ - **One YAML file.** `jiplak.yaml`, one line to start.
32
+ - **Dark mode.** Follows the `.dark` class (Tailwind convention).
33
+ - **Tiny runtime.** One React component; the capture tooling never ships to the browser.
34
+
35
+ ## Contents
36
+
37
+ - [Quick start](#quick-start)
38
+ - [FAQ](#faq)
39
+ - [Configuration](#configuration)
40
+ - [API](#api)
41
+ - [How it works](#how-it-works)
42
+ - [Sponsor](#sponsor)
43
+ - [Author](#author)
44
+ - [License](#license)
45
+
46
+ ## Quick start
47
+
48
+ ### 1. Install
49
+
50
+ ```bash
51
+ npm install jiplak
52
+ # or
53
+ bun add jiplak
54
+ ```
55
+
56
+ ### 2. Wrap components that wait for data
57
+
58
+ ```tsx
59
+ import { Skeleton } from 'jiplak/react'
60
+
61
+ function Profile() {
62
+ const { data, isLoading } = useProfile()
63
+
64
+ return (
65
+ <Skeleton name="profile-card" loading={isLoading}>
66
+ <ProfileCard user={data} />
67
+ </Skeleton>
68
+ )
69
+ }
70
+ ```
71
+
72
+ ### 3. Add `jiplak.yaml` to your project root
73
+
74
+ ```yaml
75
+ url: http://localhost:3000
76
+ ```
77
+
78
+ ### 4. Start your app, then capture
79
+
80
+ ```bash
81
+ npm run dev
82
+ ```
83
+
84
+ In a second terminal:
85
+
86
+ ```bash
87
+ npx jiplak build
88
+ ```
89
+
90
+ ### 5. Import the generated registry once
91
+
92
+ Import it from a client component that always loads, such as your providers:
93
+
94
+ ```tsx
95
+ 'use client'
96
+ import './bones/registry'
97
+ ```
98
+
99
+ Done. While `loading` is `true`, the skeleton shows automatically.
100
+
101
+ ## FAQ
102
+
103
+ <details>
104
+ <summary><b>Do I wrap every screen?</b></summary>
105
+
106
+ No. Wrap only the parts that wait for data, such as cards, lists, or tables.
107
+ Static parts like headers and menus need nothing. One screen can hold several
108
+ `<Skeleton>` components with different names.
109
+
110
+ </details>
111
+
112
+ <details>
113
+ <summary><b>Is the skeleton really generated automatically?</b></summary>
114
+
115
+ Yes. `jiplak build` crawls your app through its links, finds every
116
+ `<Skeleton>`, and records its shape. Your only job is to wrap components.
117
+
118
+ </details>
119
+
120
+ <details>
121
+ <summary><b>When do I run <code>jiplak build</code> again?</b></summary>
122
+
123
+ After a wrapped component changes its layout, or after you add a new
124
+ `<Skeleton>`. Commit the `src/bones/` folder so your team does not need to
125
+ rebuild.
126
+
127
+ </details>
128
+
129
+ <details>
130
+ <summary><b>What happens before the first build?</b></summary>
131
+
132
+ Your app works as usual. Components render normally, just without a
133
+ skeleton.
134
+
135
+ </details>
136
+
137
+ <details>
138
+ <summary><b>My component is empty without data.</b></summary>
139
+
140
+ Pass sample content through `fixture`. jiplak captures the fixture instead of
141
+ the real children:
142
+
143
+ ```tsx
144
+ <Skeleton name="profile-card" loading={isLoading} fixture={<ProfileCard user={sampleUser} />}>
145
+ <ProfileCard user={data} />
146
+ </Skeleton>
147
+ ```
148
+
149
+ </details>
150
+
151
+ <details>
152
+ <summary><b>A page was not found.</b></summary>
153
+
154
+ The crawler finds only pages that another page links to. Add unlinked pages
155
+ as extra start points:
156
+
157
+ ```yaml
158
+ routes:
159
+ - /
160
+ - /settings/profile
161
+ ```
162
+
163
+ </details>
164
+
165
+ <details>
166
+ <summary><b>Some pages must never be visited.</b></summary>
167
+
168
+ Logout links and files (pdf, images, csv) are always skipped. Skip other
169
+ paths with `ignore`:
170
+
171
+ ```yaml
172
+ ignore:
173
+ - /admin
174
+ ```
175
+
176
+ </details>
177
+
178
+ <details>
179
+ <summary><b>The build stopped at <code>maxPages</code>.</b></summary>
180
+
181
+ The crawler stops at 50 pages so dynamic routes (`/post/1`, `/post/2`, …)
182
+ cannot run forever. Raise the limit if needed:
183
+
184
+ ```yaml
185
+ maxPages: 200
186
+ ```
187
+
188
+ </details>
189
+
190
+ <details>
191
+ <summary><b>My pages need a login.</b></summary>
192
+
193
+ Send a session cookie:
194
+
195
+ ```yaml
196
+ cookies:
197
+ - session=your-session-cookie
198
+ ```
199
+
200
+ </details>
201
+
202
+ <details>
203
+ <summary><b>Browser not found.</b></summary>
204
+
205
+ Use the Chrome you already have:
206
+
207
+ ```yaml
208
+ channel: chrome
209
+ ```
210
+
211
+ Or download Chromium once:
212
+
213
+ ```bash
214
+ npx playwright install chromium
215
+ ```
216
+
217
+ </details>
218
+
219
+ ## Configuration
220
+
221
+ Every key is optional. Values below are the defaults. Unknown keys fail with
222
+ a clear error, so typos never pass silently.
223
+
224
+ ```yaml
225
+ # capture
226
+ url: http://localhost:3000 # your running app
227
+ routes: [/] # crawl start points
228
+ ignore: [] # path prefixes never visited
229
+ maxPages: 50 # crawl limit
230
+ breakpoints: [375, 768, 1280] # screen widths to capture
231
+ out: ./src/bones # output folder
232
+ wait: 800 # ms to wait after page load
233
+ channel: chrome # optional: use installed Chrome
234
+ cookies: [] # optional: name=value
235
+
236
+ # look (props on <Skeleton> override these)
237
+ animate: pulse # pulse | shimmer | solid
238
+ color: "#ececec" # light mode
239
+ darkColor: "#262626" # dark mode (.dark class)
240
+ shimmerColor: "#f7f7f7"
241
+ darkShimmerColor: "#323232"
242
+ speed: 1.6s
243
+ ```
244
+
245
+ ## API
246
+
247
+ ### `<Skeleton>`
248
+
249
+ | Prop | Required | Description |
250
+ | --- | --- | --- |
251
+ | `name` | yes | Unique name: letters, digits, `-`, `_` |
252
+ | `loading` | yes | `true` while data is loading |
253
+ | `fixture` | no | Sample content captured at build time |
254
+ | `animate`, `color`, `darkColor`, `shimmerColor`, `darkShimmerColor`, `speed` | no | Override `jiplak.yaml` for this skeleton |
255
+ | `options` | no | `{ exclude: ['.badge'] }` skips elements; `{ leafTags: ['section'] }` turns a tag into one bone |
256
+ | `bones` | no | Bones passed directly instead of the registry |
257
+ | `className` | no | Class for the wrapper element |
258
+
259
+ ### Functions
260
+
261
+ | Function | Description |
262
+ | --- | --- |
263
+ | `registerBones(map)` | Register bones by name. The generated registry calls it. |
264
+ | `configureJiplak(theme)` | Set global look defaults. The generated registry calls it with your YAML values. |
265
+
266
+ ### CLI
267
+
268
+ ```bash
269
+ jiplak build [url] [--config jiplak.yaml]
270
+ ```
271
+
272
+ ## How it works
273
+
274
+ 1. `jiplak build` opens your app in a headless browser, starting at
275
+ `routes`, and follows links on the same origin.
276
+ 2. At each breakpoint, every `<Skeleton>` on the page is captured. Text
277
+ blocks, images, and controls become rectangles ("bones").
278
+ 3. Bones are saved to `src/bones/<name>.bones.json`, plus a `registry.ts`
279
+ that loads them.
280
+ 4. At runtime, while `loading` is `true`, the bones are drawn over the hidden
281
+ component, using the breakpoint that fits the container width.
282
+
283
+ ## Development
284
+
285
+ ```bash
286
+ bun install
287
+ bun test
288
+ bun run typecheck
289
+ bun run build
290
+ ```
291
+
292
+ Issues and pull requests are welcome on
293
+ [GitHub](https://github.com/ItPohgero/jiplak/issues).
294
+
295
+ ## Sponsor
296
+
297
+ <a href="https://lokio.link">
298
+ <img src="https://raw.githubusercontent.com/ItPohgero/jiplak/main/.github/assets/lokio.png" alt="Lokio" width="72" />
299
+ </a>
300
+
301
+ jiplak is proudly sponsored by **[Lokio](https://lokio.link)**.
302
+
303
+ Like jiplak? Support its development:
304
+
305
+ [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-FFDD00?style=for-the-badge&logo=buymeacoffee&logoColor=black)](https://buymeacoffee.com/wahyuagusarifin)
306
+
307
+ ## Author
308
+
309
+ **Wahyu Agus Arifin** · [@ItPohgero](https://github.com/ItPohgero)
310
+
311
+ ## License
312
+
313
+ [MIT](./LICENSE) © 2026 Wahyu Agus Arifin
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { parseArgs } from 'node:util';
5
+ import { chromium } from 'playwright';
6
+ import { crawlPaths, loadConfig, registrySource, themeOf, validName } from './config.js';
7
+ const USAGE = `Usage: jiplak build [url] [--config jiplak.yaml]
8
+
9
+ Starts at each route in jiplak.yaml, follows same-origin links, captures
10
+ every <Skeleton name="..."> at every breakpoint, and writes
11
+ <out>/<name>.bones.json plus <out>/registry.ts.`;
12
+ const { positionals, values } = parseArgs({
13
+ allowPositionals: true,
14
+ options: { config: { type: 'string', default: 'jiplak.yaml' }, help: { type: 'boolean', short: 'h' } },
15
+ });
16
+ if (values.help || positionals[0] !== 'build') {
17
+ console.log(USAGE);
18
+ process.exit(values.help ? 0 : 1);
19
+ }
20
+ let configText = '';
21
+ try {
22
+ configText = readFileSync(resolve(values.config), 'utf8');
23
+ }
24
+ catch {
25
+ console.warn(`jiplak: ${values.config} not found, using defaults`);
26
+ }
27
+ const config = loadConfig(configText);
28
+ const origin = (positionals[1] ?? config.url).replace(/\/$/, '');
29
+ const out = resolve(config.out);
30
+ const browser = await chromium.launch({ channel: config.channel });
31
+ const context = await browser.newContext();
32
+ await context.addInitScript(() => {
33
+ window.__JIPLAK_BUILD = true;
34
+ });
35
+ if (config.cookies.length > 0) {
36
+ await context.addCookies(config.cookies.map(c => {
37
+ const i = c.indexOf('=');
38
+ return { name: c.slice(0, i), value: c.slice(i + 1), url: origin };
39
+ }));
40
+ }
41
+ const captured = {};
42
+ const page = await context.newPage();
43
+ const queue = [...new Set(config.routes)];
44
+ const seen = new Set(queue);
45
+ try {
46
+ for (let route = queue.shift(); route !== undefined; route = queue.shift()) {
47
+ for (const [i, width] of config.breakpoints.entries()) {
48
+ await page.setViewportSize({ width, height: 900 });
49
+ await page.goto(origin + route, { waitUntil: 'load' });
50
+ await page.waitForTimeout(config.wait);
51
+ if (i === 0) {
52
+ if (!page.url().startsWith(origin)) {
53
+ console.warn(` ${route}: redirected off-origin to ${page.url()}, skipped`);
54
+ break;
55
+ }
56
+ const hrefs = await page.$$eval('a[href]', as => as.map(a => a.href));
57
+ for (const path of crawlPaths(hrefs, origin, config.ignore)) {
58
+ if (seen.size >= config.maxPages)
59
+ break;
60
+ if (!seen.has(path)) {
61
+ seen.add(path);
62
+ queue.push(path);
63
+ }
64
+ }
65
+ }
66
+ const found = await page.evaluate(() => {
67
+ const snap = window.__JIPLAK_SNAPSHOT;
68
+ if (!snap)
69
+ return null;
70
+ const result = {};
71
+ for (const el of document.querySelectorAll('[data-jiplak]')) {
72
+ const opts = el.dataset.jiplakOptions ? JSON.parse(el.dataset.jiplakOptions) : {};
73
+ result[el.dataset.jiplak] = snap(el, opts);
74
+ }
75
+ return result;
76
+ });
77
+ if (!found) {
78
+ continue; // page does not load jiplak/react
79
+ }
80
+ for (const [name, capture] of Object.entries(found)) {
81
+ if (!validName(name)) {
82
+ console.warn(` skip "${name}": use letters, digits, - or _`);
83
+ continue;
84
+ }
85
+ ;
86
+ (captured[name] ??= {})[width] = capture;
87
+ }
88
+ if (Object.keys(found).length > 0)
89
+ console.log(` ${route} @${width}: ${Object.keys(found).join(', ')}`);
90
+ }
91
+ }
92
+ }
93
+ finally {
94
+ await browser.close();
95
+ }
96
+ mkdirSync(out, { recursive: true });
97
+ for (const [name, bones] of Object.entries(captured)) {
98
+ writeFileSync(join(out, `${name}.bones.json`), JSON.stringify(bones) + '\n');
99
+ }
100
+ // Skeletons not visited this run keep their previous bones.
101
+ const names = readdirSync(out)
102
+ .filter(f => f.endsWith('.bones.json'))
103
+ .map(f => f.slice(0, -'.bones.json'.length));
104
+ writeFileSync(join(out, 'registry.ts'), registrySource(names, themeOf(config)));
105
+ if (seen.size >= config.maxPages)
106
+ console.warn(`jiplak: stopped at maxPages (${config.maxPages}); raise it in jiplak.yaml if pages were missed`);
107
+ console.log(`jiplak: ${seen.size} page(s) crawled, ${Object.keys(captured).length} skeleton(s) captured, ${names.length} in ${config.out}/registry.ts`);
@@ -0,0 +1,30 @@
1
+ import type { Theme } from './core.js';
2
+ export interface Config extends Theme {
3
+ /** Origin of the running app, e.g. http://localhost:3000. */
4
+ url: string;
5
+ /** Start paths. Same-origin links found on each page are followed too. */
6
+ routes: string[];
7
+ /** Upper bound on pages visited, so dynamic routes (/item/1, /item/2, ...) cannot run forever. */
8
+ maxPages: number;
9
+ /** Path prefixes never visited, e.g. /admin. Logout links are always skipped. */
10
+ ignore: string[];
11
+ /** Viewport widths captured per route. */
12
+ breakpoints: number[];
13
+ /** Output directory for `<name>.bones.json` and `registry.ts`. */
14
+ out: string;
15
+ /** ms to wait after load before capture. */
16
+ wait: number;
17
+ /** Playwright browser channel, e.g. `chrome` to reuse installed Chrome instead of downloading Chromium. */
18
+ channel?: string;
19
+ /** Cookies sent with every page, `name=value`. */
20
+ cookies: string[];
21
+ }
22
+ /** Parse and validate jiplak.yaml text; unknown keys are rejected so typos surface. */
23
+ export declare function loadConfig(text: string): Config;
24
+ export declare function themeOf(config: Config): Theme;
25
+ /** Source of the generated registry: imports every bones file and applies the yaml theme. */
26
+ export declare function registrySource(names: string[], theme: Theme): string;
27
+ /** Same-origin paths from link hrefs worth crawling: no query/hash, no logout, no files, no ignored prefixes. */
28
+ export declare function crawlPaths(hrefs: string[], origin: string, ignore: string[]): string[];
29
+ /** Skeleton names become file names; keep them safe for the filesystem and imports. */
30
+ export declare const validName: (name: string) => boolean;
package/dist/config.js ADDED
@@ -0,0 +1,108 @@
1
+ import { parse } from 'yaml';
2
+ const DEFAULTS = {
3
+ url: 'http://localhost:3000',
4
+ routes: ['/'],
5
+ maxPages: 50,
6
+ ignore: [],
7
+ breakpoints: [375, 768, 1280],
8
+ out: './src/bones',
9
+ wait: 800,
10
+ cookies: [],
11
+ };
12
+ const THEME_KEYS = ['color', 'darkColor', 'shimmerColor', 'darkShimmerColor', 'speed'];
13
+ const ANIMATIONS = ['pulse', 'shimmer', 'solid'];
14
+ function fail(msg) {
15
+ throw new Error(`jiplak.yaml: ${msg}`);
16
+ }
17
+ const isStrings = (v) => Array.isArray(v) && v.every(s => typeof s === 'string');
18
+ /** Parse and validate jiplak.yaml text; unknown keys are rejected so typos surface. */
19
+ export function loadConfig(text) {
20
+ const raw = parse(text) ?? {};
21
+ if (typeof raw !== 'object' || Array.isArray(raw))
22
+ fail('root must be a mapping');
23
+ const c = { ...DEFAULTS, ...raw };
24
+ const known = new Set([...Object.keys(DEFAULTS), ...THEME_KEYS, 'animate', 'channel']);
25
+ for (const key of Object.keys(c))
26
+ if (!known.has(key))
27
+ fail(`unknown key "${key}"`);
28
+ if (typeof c.url !== 'string' || !/^https?:\/\//.test(c.url))
29
+ fail('url must start with http:// or https://');
30
+ if (!isStrings(c.routes) || c.routes.length === 0)
31
+ fail('routes must be a non-empty list of paths');
32
+ if (c.routes.some(r => !r.startsWith('/')))
33
+ fail('every route must start with "/"');
34
+ if (!Number.isInteger(c.maxPages) || c.maxPages < 1)
35
+ fail('maxPages must be a positive integer');
36
+ if (!isStrings(c.ignore) || c.ignore.some(r => !r.startsWith('/')))
37
+ fail('ignore must be a list of paths starting with "/"');
38
+ const bps = c.breakpoints;
39
+ if (!Array.isArray(bps) || bps.length === 0 || !bps.every(b => Number.isInteger(b) && b > 0))
40
+ fail('breakpoints must be a non-empty list of positive integers');
41
+ if (typeof c.out !== 'string' || c.out === '')
42
+ fail('out must be a path');
43
+ if (typeof c.wait !== 'number' || c.wait < 0)
44
+ fail('wait must be a number >= 0');
45
+ if (c.channel !== undefined && typeof c.channel !== 'string')
46
+ fail('channel must be a string');
47
+ if (!isStrings(c.cookies) || c.cookies.some(s => !s.includes('=')))
48
+ fail('cookies must be a list of name=value');
49
+ for (const key of THEME_KEYS)
50
+ if (c[key] !== undefined && typeof c[key] !== 'string')
51
+ fail(`${key} must be a string`);
52
+ if (c.animate !== undefined && !ANIMATIONS.includes(c.animate))
53
+ fail(`animate must be one of ${ANIMATIONS.join(', ')}`);
54
+ return c;
55
+ }
56
+ export function themeOf(config) {
57
+ const theme = {};
58
+ for (const key of THEME_KEYS)
59
+ if (config[key] !== undefined)
60
+ theme[key] = config[key];
61
+ if (config.animate)
62
+ theme.animate = config.animate;
63
+ return theme;
64
+ }
65
+ /** Source of the generated registry: imports every bones file and applies the yaml theme. */
66
+ export function registrySource(names, theme) {
67
+ const sorted = [...names].sort();
68
+ return [
69
+ '// Generated by `jiplak build`. Do not edit.',
70
+ "'use client'",
71
+ '',
72
+ "import { configureJiplak, registerBones, type Bones } from 'jiplak/react'",
73
+ ...sorted.map((n, i) => `import b${i} from './${n}.bones.json'`),
74
+ '',
75
+ `configureJiplak(${JSON.stringify(theme)})`,
76
+ '',
77
+ 'registerBones({',
78
+ ...sorted.map((n, i) => ` ${JSON.stringify(n)}: b${i} as unknown as Bones,`),
79
+ '})',
80
+ '',
81
+ ].join('\n');
82
+ }
83
+ // Visiting these with a GET can end the session or download files.
84
+ const NEVER_VISIT = /(log-?out|sign-?out|keluar)(\/|$)|\.(pdf|zip|png|jpe?g|gif|svg|webp|mp4|csv|xlsx?)$/i;
85
+ /** Same-origin paths from link hrefs worth crawling: no query/hash, no logout, no files, no ignored prefixes. */
86
+ export function crawlPaths(hrefs, origin, ignore) {
87
+ const paths = new Set();
88
+ for (const href of hrefs) {
89
+ let u;
90
+ try {
91
+ u = new URL(href, origin);
92
+ }
93
+ catch {
94
+ continue;
95
+ }
96
+ if (u.origin !== new URL(origin).origin)
97
+ continue;
98
+ const path = u.pathname.replace(/(.)\/$/, '$1');
99
+ if (NEVER_VISIT.test(path))
100
+ continue;
101
+ if (ignore.some(p => path === p || path.startsWith(p.replace(/\/$/, '') + '/')))
102
+ continue;
103
+ paths.add(path);
104
+ }
105
+ return [...paths];
106
+ }
107
+ /** Skeleton names become file names; keep them safe for the filesystem and imports. */
108
+ export const validName = (name) => /^[a-z0-9][a-z0-9-_]*$/i.test(name);
package/dist/core.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ /** One placeholder rect: x and w are % of capture width, y and h are px, r is border-radius. */
2
+ export type Bone = [x: number, y: number, w: number, h: number, r: number | string];
3
+ /** Layout of one `<Skeleton>` captured at one viewport width. */
4
+ export interface Capture {
5
+ width: number;
6
+ height: number;
7
+ bones: Bone[];
8
+ }
9
+ /** Captures keyed by viewport width, as written to `<name>.bones.json`. */
10
+ export type Bones = Record<string, Capture>;
11
+ export type Animate = 'pulse' | 'shimmer' | 'solid';
12
+ /** Runtime look, shared by jiplak.yaml and `configureJiplak`. */
13
+ export interface Theme {
14
+ color?: string;
15
+ darkColor?: string;
16
+ shimmerColor?: string;
17
+ darkShimmerColor?: string;
18
+ animate?: Animate;
19
+ speed?: string;
20
+ }
21
+ /**
22
+ * Pick the capture whose captured container width is nearest to `width`.
23
+ * Compares container to container (keys are viewport widths, and a container is
24
+ * usually narrower than its viewport). Nearest, not floor, so a scrollbar that
25
+ * shaves a few px off the live container still picks the matching capture.
26
+ */
27
+ export declare function pickCapture(bones: Bones, width: number): Capture | undefined;
package/dist/core.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Pick the capture whose captured container width is nearest to `width`.
3
+ * Compares container to container (keys are viewport widths, and a container is
4
+ * usually narrower than its viewport). Nearest, not floor, so a scrollbar that
5
+ * shaves a few px off the live container still picks the matching capture.
6
+ */
7
+ export function pickCapture(bones, width) {
8
+ let best;
9
+ for (const c of Object.values(bones)) {
10
+ if (!best || Math.abs(c.width - width) < Math.abs(best.width - width))
11
+ best = c;
12
+ }
13
+ return best;
14
+ }
@@ -0,0 +1,29 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type Bones, type Theme } from './core.js';
3
+ import { snapshot, type SnapshotOptions } from './snapshot.js';
4
+ export type { Bone, Bones, Capture, Theme, Animate } from './core.js';
5
+ export type { SnapshotOptions } from './snapshot.js';
6
+ export { snapshot };
7
+ declare global {
8
+ interface Window {
9
+ __JIPLAK_BUILD?: boolean;
10
+ __JIPLAK_SNAPSHOT?: typeof snapshot;
11
+ }
12
+ }
13
+ /** Called by the generated registry; may also be called by hand. */
14
+ export declare function registerBones(map: Record<string, Bones>): void;
15
+ /** Global look defaults; jiplak.yaml values land here through the generated registry. */
16
+ export declare function configureJiplak(next: Theme): void;
17
+ export interface SkeletonProps extends Theme {
18
+ /** Key used by the CLI to capture and by the runtime to look up bones. */
19
+ name: string;
20
+ loading: boolean;
21
+ children?: ReactNode;
22
+ /** Rendered instead of children while the CLI captures (mock data, no auth). */
23
+ fixture?: ReactNode;
24
+ /** Bones passed directly instead of resolved from the registry. */
25
+ bones?: Bones;
26
+ options?: SnapshotOptions;
27
+ className?: string;
28
+ }
29
+ export declare function Skeleton({ name, loading, children, fixture, bones, options, className, ...look }: SkeletonProps): import("react").JSX.Element;
package/dist/react.js ADDED
@@ -0,0 +1,77 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { pickCapture } from './core.js';
5
+ import { snapshot } from './snapshot.js';
6
+ export { snapshot };
7
+ const isBuild = typeof window !== 'undefined' && window.__JIPLAK_BUILD === true;
8
+ if (isBuild)
9
+ window.__JIPLAK_SNAPSHOT = snapshot;
10
+ const registry = new Map();
11
+ let theme = {
12
+ color: '#ececec',
13
+ darkColor: '#262626',
14
+ shimmerColor: '#f7f7f7',
15
+ darkShimmerColor: '#323232',
16
+ animate: 'pulse',
17
+ speed: '1.6s',
18
+ };
19
+ /** Called by the generated registry; may also be called by hand. */
20
+ export function registerBones(map) {
21
+ for (const [name, bones] of Object.entries(map))
22
+ registry.set(name, bones);
23
+ }
24
+ /** Global look defaults; jiplak.yaml values land here through the generated registry. */
25
+ export function configureJiplak(next) {
26
+ theme = { ...theme, ...next };
27
+ }
28
+ const KEYFRAMES = '@keyframes jiplak-pulse{50%{opacity:.45}}' +
29
+ '@keyframes jiplak-shimmer{from{background-position:200% 0}to{background-position:-200% 0}}';
30
+ export function Skeleton({ name, loading, children, fixture, bones, options, className, ...look }) {
31
+ const ref = useRef(null);
32
+ const [width, setWidth] = useState(0);
33
+ const [dark, setDark] = useState(false);
34
+ useEffect(() => {
35
+ const el = ref.current;
36
+ if (!el || isBuild)
37
+ return;
38
+ const ro = new ResizeObserver(([entry]) => setWidth(Math.round(entry.contentRect.width)));
39
+ ro.observe(el);
40
+ const checkDark = () => setDark(!!el.closest('.dark'));
41
+ checkDark();
42
+ const mo = new MutationObserver(checkDark);
43
+ mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
44
+ return () => {
45
+ ro.disconnect();
46
+ mo.disconnect();
47
+ };
48
+ }, []);
49
+ if (isBuild) {
50
+ return (_jsx("div", { ref: ref, className: className, "data-jiplak": name, "data-jiplak-options": options && JSON.stringify(options), children: fixture ?? children }));
51
+ }
52
+ const t = { ...theme, ...look };
53
+ const source = bones ?? registry.get(name);
54
+ const capture = loading && source && width > 0 ? pickCapture(source, width) : undefined;
55
+ const color = dark ? t.darkColor : t.color;
56
+ const highlight = dark ? t.darkShimmerColor : t.shimmerColor;
57
+ return (_jsxs("div", { ref: ref, className: className, "aria-busy": loading || undefined, style: { position: 'relative', minHeight: capture?.height }, children: [_jsx("div", { style: capture ? { visibility: 'hidden' } : undefined, children: children }), capture && (_jsxs("div", { "aria-hidden": true, style: { position: 'absolute', inset: 0, overflow: 'hidden', pointerEvents: 'none' }, children: [_jsx("style", { children: KEYFRAMES }), capture.bones.map(([x, y, w, h, r], i) => {
58
+ const style = {
59
+ position: 'absolute',
60
+ left: `${x}%`,
61
+ top: y,
62
+ // Circles keep their shape when the container is wider or narrower than at capture.
63
+ width: r === '50%' ? h : `${w}%`,
64
+ height: h,
65
+ borderRadius: r,
66
+ background: color,
67
+ };
68
+ if (t.animate === 'pulse')
69
+ style.animation = `jiplak-pulse ${t.speed} ease-in-out infinite`;
70
+ if (t.animate === 'shimmer') {
71
+ style.background = `linear-gradient(110deg, ${color} 30%, ${highlight} 50%, ${color} 70%)`;
72
+ style.backgroundSize = '200% 100%';
73
+ style.animation = `jiplak-shimmer ${t.speed} linear infinite`;
74
+ }
75
+ return _jsx("div", { style: style }, i);
76
+ })] }))] }));
77
+ }
@@ -0,0 +1,13 @@
1
+ import type { Capture } from './core.js';
2
+ /** Per-skeleton capture tuning, passed as `<Skeleton options>`. */
3
+ export interface SnapshotOptions {
4
+ /** Extra tags captured as one bone while they hold only inline content. */
5
+ leafTags?: string[];
6
+ /** CSS selectors skipped together with their descendants. */
7
+ exclude?: string[];
8
+ }
9
+ /**
10
+ * Walk the rendered children of `root` and record every visible leaf
11
+ * (text block, media, form control, childless element) as a bone.
12
+ */
13
+ export declare function snapshot(root: Element, options?: SnapshotOptions): Capture;
@@ -0,0 +1,50 @@
1
+ const LEAF_TAGS = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'li', 'td', 'th', 'label'];
2
+ const ATOMIC = new Set(['img', 'svg', 'video', 'canvas', 'picture', 'input', 'button', 'textarea', 'select']);
3
+ const pct = (v, total) => (total > 0 ? +((v / total) * 100).toFixed(3) : 0);
4
+ function visible(el) {
5
+ const s = getComputedStyle(el);
6
+ return s.display !== 'none' && s.visibility !== 'hidden' && s.opacity !== '0';
7
+ }
8
+ function radius(el, rect, tag) {
9
+ const square = Math.abs(rect.width - rect.height) < 4;
10
+ const s = getComputedStyle(el);
11
+ const r = parseFloat(s.borderTopLeftRadius) || 0;
12
+ if (r === 0)
13
+ return square && ATOMIC.has(tag) && tag !== 'input' && tag !== 'button' ? '50%' : 6;
14
+ if (s.borderRadius.includes('%') || r >= Math.min(rect.width, rect.height) / 2)
15
+ return square ? '50%' : 9999;
16
+ return s.borderRadius;
17
+ }
18
+ /**
19
+ * Walk the rendered children of `root` and record every visible leaf
20
+ * (text block, media, form control, childless element) as a bone.
21
+ */
22
+ export function snapshot(root, options = {}) {
23
+ const base = root.getBoundingClientRect();
24
+ const leafTags = new Set([...LEAF_TAGS, ...(options.leafTags ?? [])]);
25
+ const exclude = options.exclude?.join(',');
26
+ const bones = [];
27
+ const walk = (el) => {
28
+ if (!visible(el) || (exclude && el.matches(exclude)))
29
+ return;
30
+ const tag = el.tagName.toLowerCase();
31
+ const kids = [...el.children].filter(visible);
32
+ const inlineOnly = kids.every(k => ['inline', 'contents'].includes(getComputedStyle(k).display));
33
+ if (kids.length > 0 && !ATOMIC.has(tag) && !(leafTags.has(tag) && inlineOnly)) {
34
+ kids.forEach(walk);
35
+ return;
36
+ }
37
+ const rect = el.getBoundingClientRect();
38
+ if (rect.width < 1 || rect.height < 1)
39
+ return;
40
+ bones.push([
41
+ pct(rect.left - base.left, base.width),
42
+ Math.round(rect.top - base.top),
43
+ pct(rect.width, base.width),
44
+ Math.round(rect.height),
45
+ radius(el, rect, tag),
46
+ ]);
47
+ };
48
+ [...root.children].forEach(walk);
49
+ return { width: Math.round(base.width), height: Math.round(base.height), bones };
50
+ }
package/package.json CHANGED
@@ -1,6 +1,74 @@
1
1
  {
2
2
  "name": "jiplak",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "description": "Pixel-perfect skeleton loading screens, auto-captured from your real React UI.",
5
+ "keywords": [
6
+ "skeleton",
7
+ "skeleton-loader",
8
+ "loading",
9
+ "placeholder",
10
+ "react",
11
+ "playwright",
12
+ "yaml",
13
+ "ui"
14
+ ],
15
+ "author": "Wahyu Agus Arifin (https://github.com/ItPohgero)",
16
+ "homepage": "https://github.com/ItPohgero/jiplak#readme",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/ItPohgero/jiplak.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/ItPohgero/jiplak/issues"
23
+ },
24
+ "funding": [
25
+ {
26
+ "type": "buymeacoffee",
27
+ "url": "https://buymeacoffee.com/wahyuagusarifin"
28
+ },
29
+ {
30
+ "type": "sponsor",
31
+ "url": "https://lokio.link"
32
+ }
33
+ ],
34
+ "type": "module",
35
+ "license": "MIT",
36
+ "bin": {
37
+ "jiplak": "./dist/cli.js"
38
+ },
39
+ "exports": {
40
+ "./react": {
41
+ "types": "./dist/react.d.ts",
42
+ "import": "./dist/react.js"
43
+ },
44
+ "./package.json": "./package.json"
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "README.md",
49
+ "README.id.md",
50
+ "LICENSE"
51
+ ],
52
+ "scripts": {
53
+ "build": "tsc -p tsconfig.json",
54
+ "typecheck": "tsc -p tsconfig.json --noEmit",
55
+ "test": "bun test",
56
+ "prepublishOnly": "bun run build && bun test"
57
+ },
58
+ "dependencies": {
59
+ "playwright": "^1.56.0",
60
+ "yaml": "^2.8.0"
61
+ },
62
+ "peerDependencies": {
63
+ "react": ">=18"
64
+ },
65
+ "devDependencies": {
66
+ "@types/bun": "latest",
67
+ "@types/node": "^24.0.0",
68
+ "@types/react": "^19.0.0",
69
+ "react": "^19.0.0",
70
+ "react-dom": "^19.0.0",
71
+ "@types/react-dom": "^19.0.0",
72
+ "typescript": "^5.9.0"
73
+ }
74
+ }