@0xgf/boneyard 1.0.0

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/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # boneyard
2
+
3
+ Pixel-perfect skeleton loading screens, extracted from your real DOM.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ npm install @0xgf/boneyard
9
+ ```
10
+
11
+ ```tsx
12
+ import { Skeleton } from '@0xgf/boneyard/react'
13
+
14
+ <Skeleton name="blog-card" loading={isLoading}>
15
+ <BlogCard data={data} />
16
+ </Skeleton>
17
+ ```
18
+
19
+ ```bash
20
+ npx boneyard build
21
+ ```
22
+
23
+ ```tsx
24
+ // app/layout.tsx
25
+ import './bones/registry'
26
+ ```
27
+
28
+ Done. See the [full documentation](https://github.com/0xGF/boneyard) for all props, CLI options, and examples.
29
+
30
+ ## License
31
+
32
+ MIT
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * boneyard CLI
4
+ *
5
+ * Visits your running app at every breakpoint, captures all named <Skeleton>
6
+ * components, and writes responsive bones JSON files to disk.
7
+ *
8
+ * Usage:
9
+ * npx boneyard build [url] [options]
10
+ * npx boneyard build ← auto-detects your dev server
11
+ * npx boneyard build http://localhost:5173 ← explicit URL
12
+ * npx boneyard build http://localhost:3000/blog http://localhost:3000/shop
13
+ *
14
+ * Options:
15
+ * --out <dir> Where to write .bones.json files (default: auto-detected)
16
+ * --breakpoints <bp> Viewport widths to capture, comma-separated (default: 375,768,1280)
17
+ * --wait <ms> Extra ms to wait after page load (default: 800)
18
+ *
19
+ * Requires playwright:
20
+ * npm install -D playwright && npx playwright install chromium
21
+ */
22
+
23
+ import { writeFileSync, mkdirSync, existsSync } from 'fs'
24
+ import { resolve, join } from 'path'
25
+ import http from 'http'
26
+ import https from 'https'
27
+
28
+ const args = process.argv.slice(2)
29
+ const command = args[0]
30
+
31
+ if (!command || command === '--help' || command === '-h') {
32
+ printHelp()
33
+ process.exit(0)
34
+ }
35
+
36
+ if (command !== 'build') {
37
+ console.error(`boneyard: unknown command "${command}". Try: npx boneyard build`)
38
+ process.exit(1)
39
+ }
40
+
41
+ // ── Parse args ────────────────────────────────────────────────────────────────
42
+
43
+ const urls = []
44
+ // Auto-detect: prefer ./src/bones for projects with a src/ directory (Next.js, Vite, etc.)
45
+ let outDir = existsSync(resolve(process.cwd(), 'src')) ? './src/bones' : './bones'
46
+ let breakpoints = [375, 768, 1280]
47
+ let waitMs = 800
48
+
49
+ for (let i = 1; i < args.length; i++) {
50
+ if (args[i] === '--out') {
51
+ outDir = args[++i]
52
+ } else if (args[i] === '--breakpoints') {
53
+ breakpoints = args[++i].split(',').map(Number).filter(n => n > 0)
54
+ } else if (args[i] === '--wait') {
55
+ waitMs = Math.max(0, Number(args[++i]) || 800)
56
+ } else if (!args[i].startsWith('--')) {
57
+ urls.push(args[i])
58
+ }
59
+ }
60
+
61
+ // ── Auto-detect dev server ────────────────────────────────────────────────────
62
+
63
+ /**
64
+ * Check if a URL is responding. Returns true if we get any HTTP response
65
+ * (even 4xx/5xx — we just want to know something is listening).
66
+ */
67
+ function probe(url) {
68
+ return new Promise(resolve => {
69
+ const mod = url.startsWith('https') ? https : http
70
+ const req = mod.get(url, { timeout: 1500 }, res => {
71
+ res.destroy()
72
+ resolve(true)
73
+ })
74
+ req.on('error', () => resolve(false))
75
+ req.on('timeout', () => { req.destroy(); resolve(false) })
76
+ })
77
+ }
78
+
79
+ /** Common dev server ports in priority order */
80
+ const DEV_PORTS = [3000, 3001, 3002, 5173, 5174, 4321, 8080, 8000, 4200, 8888]
81
+
82
+ async function detectDevServer() {
83
+ for (const port of DEV_PORTS) {
84
+ const url = `http://localhost:${port}`
85
+ const ok = await probe(url)
86
+ if (ok) return url
87
+ }
88
+ return null
89
+ }
90
+
91
+ if (urls.length === 0) {
92
+ process.stdout.write(' boneyard: no URL provided — scanning for dev server...')
93
+ const detected = await detectDevServer()
94
+ if (detected) {
95
+ process.stdout.write(` found ${detected}\n`)
96
+ urls.push(detected)
97
+ } else {
98
+ process.stdout.write(' none found\n\n')
99
+ console.error(
100
+ ' boneyard: could not find a running dev server.\n\n' +
101
+ ' Start your dev server first, then run:\n' +
102
+ ' npx boneyard build\n\n' +
103
+ ' Or pass your URL explicitly:\n' +
104
+ ' npx boneyard build http://localhost:3000\n'
105
+ )
106
+ process.exit(1)
107
+ }
108
+ }
109
+
110
+ // ── Load playwright ───────────────────────────────────────────────────────────
111
+
112
+ let chromium
113
+ try {
114
+ const pw = await import('playwright')
115
+ chromium = pw.chromium
116
+ } catch {
117
+ console.error(
118
+ '\nboneyard: playwright not found.\n\n' +
119
+ 'Install it:\n' +
120
+ ' npm install -D playwright\n' +
121
+ ' npx playwright install chromium\n'
122
+ )
123
+ process.exit(1)
124
+ }
125
+
126
+ // ── Capture ───────────────────────────────────────────────────────────────────
127
+
128
+ console.log(`\n boneyard build`)
129
+ console.log(` URLs: ${urls.join(', ')}`)
130
+ console.log(` Breakpoints: ${breakpoints.join(', ')}px`)
131
+ console.log(` Output: ${outDir}\n`)
132
+
133
+ const browser = await chromium.launch()
134
+ const page = await browser.newPage()
135
+
136
+ // { [skeletonName]: { breakpoints: { [width]: SkeletonResult } } }
137
+ const collected = {}
138
+
139
+ for (const url of urls) {
140
+ console.log(` Visiting ${url}`)
141
+
142
+ for (const width of breakpoints) {
143
+ await page.setViewportSize({ width, height: 900 })
144
+
145
+ try {
146
+ await page.goto(url, { waitUntil: 'networkidle', timeout: 15_000 })
147
+ } catch {
148
+ // networkidle can timeout on heavy pages — still try to capture
149
+ }
150
+
151
+ // Wait for React to render and boneyard to snapshot
152
+ if (waitMs > 0) await page.waitForTimeout(waitMs)
153
+
154
+ // Read the global registry that <Skeleton name="..."> populates
155
+ const bones = await page.evaluate(() => {
156
+ return window.__BONEYARD_BONES ?? {}
157
+ })
158
+
159
+ const names = Object.keys(bones)
160
+
161
+ if (names.length === 0) {
162
+ console.warn(` ⚠ No named <Skeleton name="..."> found at ${width}px`)
163
+ console.warn(` Make sure your <Skeleton> has a name prop and loading=false`)
164
+ continue
165
+ }
166
+
167
+ for (const name of names) {
168
+ collected[name] ??= { breakpoints: {} }
169
+ collected[name].breakpoints[width] = bones[name]
170
+ const boneCount = bones[name].bones?.length ?? 0
171
+ console.log(` ✓ ${name} @ ${width}px (${boneCount} bones)`)
172
+ }
173
+ }
174
+ }
175
+
176
+ await browser.close()
177
+
178
+ // ── Write files ───────────────────────────────────────────────────────────────
179
+
180
+ if (Object.keys(collected).length === 0) {
181
+ console.error(
182
+ '\n boneyard: nothing captured.\n\n' +
183
+ ' Make sure your components have <Skeleton name="my-component" loading={false}>\n' +
184
+ ' so boneyard can snapshot them before the CLI reads the registry.\n'
185
+ )
186
+ process.exit(1)
187
+ }
188
+
189
+ const outputDir = resolve(process.cwd(), outDir)
190
+ mkdirSync(outputDir, { recursive: true })
191
+
192
+ console.log('')
193
+ for (const [name, data] of Object.entries(collected)) {
194
+ const outPath = join(outputDir, `${name}.bones.json`)
195
+ writeFileSync(outPath, JSON.stringify(data, null, 2))
196
+ const bpCount = Object.keys(data.breakpoints).length
197
+ console.log(` Wrote ${outPath} (${bpCount} breakpoint${bpCount !== 1 ? 's' : ''})`)
198
+ }
199
+
200
+ // ── Generate registry.js ─────────────────────────────────────────────────────
201
+ const names = Object.keys(collected)
202
+ const registryLines = [
203
+ '// Auto-generated by `npx boneyard build` — do not edit',
204
+ "import { registerBones } from '@0xgf/boneyard/react'",
205
+ '',
206
+ ]
207
+ for (const name of names) {
208
+ const varName = '_' + name.replace(/[^a-zA-Z0-9]/g, '_')
209
+ registryLines.push(`import ${varName} from './${name}.bones.json'`)
210
+ }
211
+ registryLines.push('')
212
+ registryLines.push('registerBones({')
213
+ for (const name of names) {
214
+ const varName = '_' + name.replace(/[^a-zA-Z0-9]/g, '_')
215
+ registryLines.push(` "${name}": ${varName},`)
216
+ }
217
+ registryLines.push('})')
218
+ registryLines.push('')
219
+
220
+ const registryPath = join(outputDir, 'registry.js')
221
+ writeFileSync(registryPath, registryLines.join('\n'))
222
+ console.log(` Wrote ${registryPath} (${names.length} skeleton${names.length !== 1 ? 's' : ''})`)
223
+
224
+ const count = names.length
225
+ console.log(`\n ✓ ${count} skeleton${count !== 1 ? 's' : ''} captured.\n`)
226
+
227
+ // Check if this looks like a first-time setup
228
+ const isFirstRun = !existsSync(resolve(outputDir, 'registry.js'))
229
+ console.log(` Add this once to your app entry point:`)
230
+ console.log(` import '${outDir}/registry'\n`)
231
+ console.log(` Then just use:`)
232
+ console.log(` <Skeleton name="my-component" loading={isLoading}>`)
233
+ console.log(` <MyComponent />`)
234
+ console.log(` </Skeleton>\n`)
235
+ console.log(` No initialBones import needed — boneyard resolves it from the registry.\n`)
236
+
237
+
238
+ // ── Help ──────────────────────────────────────────────────────────────────────
239
+
240
+ function printHelp() {
241
+ console.log(`
242
+ boneyard build [url] [options]
243
+
244
+ Visits your app in a headless browser, captures all named <Skeleton>
245
+ components, and writes .bones.json files + a registry to disk.
246
+
247
+ Auto-detects your dev server if no URL is given (scans ports 3000, 5173, etc.).
248
+
249
+ Options:
250
+ --out <dir> Output directory (default: ./src/bones)
251
+ --breakpoints <bp> Comma-separated px widths (default: 375,768,1280)
252
+ --wait <ms> Extra wait after page load (default: 800)
253
+
254
+ Examples:
255
+ npx boneyard build
256
+ npx boneyard build http://localhost:5173
257
+ npx boneyard build --breakpoints 390,820,1440 --out ./public/bones
258
+
259
+ Setup:
260
+ 1. Wrap your component:
261
+ <Skeleton name="blog-card" loading={isLoading}>
262
+ <BlogCard />
263
+ </Skeleton>
264
+
265
+ 2. Run: npx boneyard build
266
+
267
+ 3. Import the registry once in your app entry:
268
+ import './bones/registry'
269
+
270
+ Done. Every <Skeleton name="..."> auto-resolves its bones.
271
+ `)
272
+ }
@@ -0,0 +1,15 @@
1
+ import type { SkeletonDescriptor, SkeletonResult, SnapshotConfig } from './types.js';
2
+ /**
3
+ * Snapshot the exact visual layout of a rendered DOM element as skeleton bones.
4
+ * Walks the DOM, finds every visible leaf element, and captures its exact
5
+ * bounding rect relative to the root. No layout engine, no heuristics —
6
+ * just pixel-perfect positions read directly from the browser.
7
+ *
8
+ * const { bones, width, height } = snapshotBones(el, 'my-component', config)
9
+ */
10
+ export declare function snapshotBones(el: Element, name?: string, config?: SnapshotConfig): SkeletonResult;
11
+ /**
12
+ * Extract a SkeletonDescriptor from a rendered DOM element.
13
+ * Reads computed styles — no config needed. Just point it at your component.
14
+ */
15
+ export declare function fromElement(el: Element): SkeletonDescriptor;