@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 +32 -0
- package/bin/boneyard.mjs +272 -0
- package/dist/extract.d.ts +15 -0
- package/dist/extract.js +380 -0
- package/dist/index.d.ts +54 -0
- package/dist/index.js +57 -0
- package/dist/layout.d.ts +15 -0
- package/dist/layout.js +256 -0
- package/dist/react.d.ts +107 -0
- package/dist/react.js +146 -0
- package/dist/responsive.d.ts +34 -0
- package/dist/responsive.js +67 -0
- package/dist/runtime.d.ts +17 -0
- package/dist/runtime.js +38 -0
- package/dist/types.d.ts +170 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
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
|
package/bin/boneyard.mjs
ADDED
|
@@ -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;
|