@lukas_holdings/castdom 1.0.3 → 1.0.4
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/bin/castdom.js +0 -0
- package/dist/astro.cjs +13 -31
- package/dist/astro.cjs.map +1 -1
- package/dist/astro.d.cts +4 -8
- package/dist/astro.d.ts +4 -8
- package/dist/astro.js +7 -25
- package/dist/astro.js.map +1 -1
- package/dist/{chunk-M4OXJTRQ.js → chunk-COLESJ66.js} +3 -3
- package/dist/{chunk-M4OXJTRQ.js.map → chunk-COLESJ66.js.map} +1 -1
- package/dist/chunk-EJRNKHL5.js +31 -0
- package/dist/chunk-EJRNKHL5.js.map +1 -0
- package/dist/{chunk-C3VW72Z3.cjs → chunk-JRQ6EVQP.cjs} +2 -10
- package/dist/chunk-JRQ6EVQP.cjs.map +1 -0
- package/dist/{chunk-V4FV5XFF.js → chunk-KGLTVTHU.js} +4 -4
- package/dist/{chunk-V4FV5XFF.js.map → chunk-KGLTVTHU.js.map} +1 -1
- package/dist/{chunk-CC4LCPVY.cjs → chunk-O4OOMGGM.cjs} +8 -8
- package/dist/{chunk-CC4LCPVY.cjs.map → chunk-O4OOMGGM.cjs.map} +1 -1
- package/dist/{chunk-6RFGWOGG.js → chunk-ONS533CQ.js} +3 -3
- package/dist/{chunk-6RFGWOGG.js.map → chunk-ONS533CQ.js.map} +1 -1
- package/dist/{chunk-BDIAGFG5.cjs → chunk-ORY4OMZ5.cjs} +4 -4
- package/dist/{chunk-BDIAGFG5.cjs.map → chunk-ORY4OMZ5.cjs.map} +1 -1
- package/dist/{chunk-C2D4NZQB.cjs → chunk-QLEBTZIB.cjs} +7 -7
- package/dist/{chunk-C2D4NZQB.cjs.map → chunk-QLEBTZIB.cjs.map} +1 -1
- package/dist/{chunk-ASS2BFPN.cjs → chunk-XS5HAU5E.cjs} +8 -8
- package/dist/{chunk-ASS2BFPN.cjs.map → chunk-XS5HAU5E.cjs.map} +1 -1
- package/dist/{chunk-W236FF4E.cjs → chunk-YDT4TPB7.cjs} +11 -11
- package/dist/{chunk-W236FF4E.cjs.map → chunk-YDT4TPB7.cjs.map} +1 -1
- package/dist/{chunk-275VEEA7.js → chunk-ZBJB7WVV.js} +4 -4
- package/dist/{chunk-275VEEA7.js.map → chunk-ZBJB7WVV.js.map} +1 -1
- package/dist/{chunk-GVFBT6MD.js → chunk-ZWZ5ZLJE.js} +3 -3
- package/dist/{chunk-GVFBT6MD.js.map → chunk-ZWZ5ZLJE.js.map} +1 -1
- package/dist/cli.js +815 -51
- package/dist/index.cjs +48 -48
- package/dist/index.js +8 -8
- package/dist/next.cjs +15 -15
- package/dist/next.cjs.map +1 -1
- package/dist/next.d.cts +3 -3
- package/dist/next.d.ts +3 -3
- package/dist/next.js +6 -6
- package/dist/next.js.map +1 -1
- package/dist/react.cjs +9 -9
- package/dist/react.js +5 -5
- package/dist/ssr.cjs +12 -12
- package/dist/ssr.js +3 -3
- package/dist/vite.cjs +3 -3
- package/dist/vite.cjs.map +1 -1
- package/dist/vite.d.cts +1 -1
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +2 -2
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-4LFW65DU.js +0 -38
- package/dist/chunk-4LFW65DU.js.map +0 -1
- package/dist/chunk-C3VW72Z3.cjs.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,46 +1,577 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
var
|
|
2
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
3
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
4
|
+
}) : x)(function(x) {
|
|
5
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// src/cli.ts
|
|
10
|
+
import { resolve, join } from "path";
|
|
11
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
12
|
+
|
|
13
|
+
// src/core/types.ts
|
|
14
|
+
var DEFAULTS = {
|
|
15
|
+
outDir: ".castdom",
|
|
16
|
+
breakpoints: [375, 768, 1280],
|
|
17
|
+
color: "#e0e0e0",
|
|
18
|
+
shimmerColor: "#f0f0f0",
|
|
19
|
+
animationDuration: 1500,
|
|
20
|
+
contentAware: true,
|
|
21
|
+
minBoneSize: 4,
|
|
22
|
+
classPrefix: "castdom",
|
|
23
|
+
inlineStyles: false,
|
|
24
|
+
ssr: true
|
|
25
|
+
};
|
|
26
|
+
var BONE_KIND_INDEX = {
|
|
27
|
+
text: 1,
|
|
28
|
+
heading: 2,
|
|
29
|
+
image: 3,
|
|
30
|
+
avatar: 4,
|
|
31
|
+
button: 5,
|
|
32
|
+
input: 6,
|
|
33
|
+
icon: 7,
|
|
34
|
+
divider: 8,
|
|
35
|
+
block: 0
|
|
36
|
+
};
|
|
37
|
+
var BONE_KIND_FROM_INDEX = Object.fromEntries(
|
|
38
|
+
Object.entries(BONE_KIND_INDEX).map(([k, v]) => [v, k])
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// src/core/extractor.ts
|
|
42
|
+
var INLINE_TAGS = /* @__PURE__ */ new Set([
|
|
43
|
+
"A",
|
|
44
|
+
"ABBR",
|
|
45
|
+
"B",
|
|
46
|
+
"BDO",
|
|
47
|
+
"BR",
|
|
48
|
+
"CITE",
|
|
49
|
+
"CODE",
|
|
50
|
+
"DFN",
|
|
51
|
+
"EM",
|
|
52
|
+
"I",
|
|
53
|
+
"KBD",
|
|
54
|
+
"MARK",
|
|
55
|
+
"Q",
|
|
56
|
+
"S",
|
|
57
|
+
"SAMP",
|
|
58
|
+
"SMALL",
|
|
59
|
+
"SPAN",
|
|
60
|
+
"STRONG",
|
|
61
|
+
"SUB",
|
|
62
|
+
"SUP",
|
|
63
|
+
"TIME",
|
|
64
|
+
"U",
|
|
65
|
+
"VAR",
|
|
66
|
+
"WBR"
|
|
67
|
+
]);
|
|
68
|
+
var SKIP_TAGS = /* @__PURE__ */ new Set([
|
|
69
|
+
"SCRIPT",
|
|
70
|
+
"STYLE",
|
|
71
|
+
"NOSCRIPT",
|
|
72
|
+
"TEMPLATE",
|
|
73
|
+
"SVG",
|
|
74
|
+
"CANVAS",
|
|
75
|
+
"VIDEO",
|
|
76
|
+
"AUDIO",
|
|
77
|
+
"IFRAME",
|
|
78
|
+
"OBJECT",
|
|
79
|
+
"EMBED"
|
|
80
|
+
]);
|
|
81
|
+
function detectKind(el) {
|
|
82
|
+
const tag = el.tagName;
|
|
83
|
+
if (tag === "IMG" || tag === "PICTURE") {
|
|
84
|
+
const w = el.getBoundingClientRect().width;
|
|
85
|
+
const h = el.getBoundingClientRect().height;
|
|
86
|
+
const ratio = Math.min(w, h) / Math.max(w, h);
|
|
87
|
+
if (ratio > 0.85 && w < 120) return "avatar";
|
|
88
|
+
return "image";
|
|
89
|
+
}
|
|
90
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return "input";
|
|
91
|
+
if (tag === "BUTTON" || el.getAttribute("role") === "button") return "button";
|
|
92
|
+
if (tag === "HR") return "divider";
|
|
93
|
+
if (/^H[1-6]$/.test(tag)) return "heading";
|
|
94
|
+
if (tag === "SVG" || tag === "svg") return "icon";
|
|
95
|
+
const style = getComputedStyle(el);
|
|
96
|
+
const borderRadius = parseInt(style.borderRadius, 10);
|
|
97
|
+
const rect = el.getBoundingClientRect();
|
|
98
|
+
if (borderRadius >= rect.width / 2 && rect.width < 120 && rect.width > 16) {
|
|
99
|
+
return "avatar";
|
|
100
|
+
}
|
|
101
|
+
if (el.childNodes.length > 0) {
|
|
102
|
+
const hasDirectText = Array.from(el.childNodes).some(
|
|
103
|
+
(n) => n.nodeType === Node.TEXT_NODE && n.textContent?.trim()
|
|
104
|
+
);
|
|
105
|
+
if (hasDirectText && INLINE_TAGS.has(tag)) return "text";
|
|
106
|
+
if (hasDirectText && tag === "P") return "text";
|
|
107
|
+
}
|
|
108
|
+
return "block";
|
|
109
|
+
}
|
|
110
|
+
function computeRadius(el, kind) {
|
|
111
|
+
if (kind === "avatar") return 9999;
|
|
112
|
+
if (kind === "button") {
|
|
113
|
+
const style2 = getComputedStyle(el);
|
|
114
|
+
return Math.min(parseInt(style2.borderRadius, 10) || 6, 9999);
|
|
115
|
+
}
|
|
116
|
+
const style = getComputedStyle(el);
|
|
117
|
+
const r = parseInt(style.borderRadius, 10);
|
|
118
|
+
return isNaN(r) ? 0 : Math.min(r, 9999);
|
|
119
|
+
}
|
|
120
|
+
function isVisible(el) {
|
|
121
|
+
const style = getComputedStyle(el);
|
|
122
|
+
if (style.display === "none") return false;
|
|
123
|
+
if (style.visibility === "hidden") return false;
|
|
124
|
+
if (parseFloat(style.opacity) === 0) return false;
|
|
125
|
+
const rect = el.getBoundingClientRect();
|
|
126
|
+
return rect.width > 0 && rect.height > 0;
|
|
127
|
+
}
|
|
128
|
+
function isLeaf(el, contentAware) {
|
|
129
|
+
const tag = el.tagName;
|
|
130
|
+
if (tag === "IMG" || tag === "PICTURE" || tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || tag === "BUTTON" || tag === "HR" || tag === "SVG" || tag === "svg") {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (contentAware) {
|
|
134
|
+
const hasOnlyText = Array.from(el.childNodes).every(
|
|
135
|
+
(n) => n.nodeType === Node.TEXT_NODE || INLINE_TAGS.has(n.tagName)
|
|
136
|
+
);
|
|
137
|
+
if (hasOnlyText && el.textContent?.trim()) return true;
|
|
138
|
+
}
|
|
139
|
+
if (el.children.length === 0) return true;
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
function extractBones(container, options = {}) {
|
|
143
|
+
const contentAware = options.contentAware ?? DEFAULTS.contentAware;
|
|
144
|
+
const minSize = options.minBoneSize ?? DEFAULTS.minBoneSize;
|
|
145
|
+
const containerRect = container.getBoundingClientRect();
|
|
146
|
+
const bones = [];
|
|
147
|
+
function walk(el) {
|
|
148
|
+
if (SKIP_TAGS.has(el.tagName)) return;
|
|
149
|
+
if (!isVisible(el)) return;
|
|
150
|
+
if (isLeaf(el, contentAware)) {
|
|
151
|
+
const rect = el.getBoundingClientRect();
|
|
152
|
+
if (rect.width < minSize || rect.height < minSize) return;
|
|
153
|
+
const kind = contentAware ? detectKind(el) : "block";
|
|
154
|
+
const r = computeRadius(el, kind);
|
|
155
|
+
bones.push({
|
|
156
|
+
x: Math.round((rect.left - containerRect.left) * 2) / 2,
|
|
157
|
+
y: Math.round((rect.top - containerRect.top) * 2) / 2,
|
|
158
|
+
w: Math.round(rect.width * 2) / 2,
|
|
159
|
+
h: Math.round(rect.height * 2) / 2,
|
|
160
|
+
r,
|
|
161
|
+
kind
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
for (const child of el.children) {
|
|
166
|
+
walk(child);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
walk(container);
|
|
170
|
+
return {
|
|
171
|
+
viewport: window.innerWidth,
|
|
172
|
+
containerWidth: Math.round(containerRect.width * 2) / 2,
|
|
173
|
+
containerHeight: Math.round(containerRect.height * 2) / 2,
|
|
174
|
+
bones
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function getExtractorScript(selector, options = {}) {
|
|
178
|
+
return `
|
|
3
179
|
(function() {
|
|
4
|
-
${
|
|
5
|
-
${
|
|
6
|
-
${
|
|
7
|
-
${
|
|
180
|
+
${isVisible.toString()}
|
|
181
|
+
${isLeaf.toString()}
|
|
182
|
+
${detectKind.toString()}
|
|
183
|
+
${computeRadius.toString()}
|
|
8
184
|
|
|
9
|
-
var INLINE_TAGS = new Set(${JSON.stringify([...
|
|
10
|
-
var SKIP_TAGS = new Set(${JSON.stringify([...
|
|
185
|
+
var INLINE_TAGS = new Set(${JSON.stringify([...INLINE_TAGS])});
|
|
186
|
+
var SKIP_TAGS = new Set(${JSON.stringify([...SKIP_TAGS])});
|
|
11
187
|
var DEFAULTS = { contentAware: true, minBoneSize: 4 };
|
|
12
188
|
|
|
13
|
-
${
|
|
189
|
+
${extractBones.toString()}
|
|
14
190
|
|
|
15
|
-
var container = document.querySelector(${JSON.stringify(
|
|
16
|
-
if (!container) throw new Error('CastDOM: Container not found: ' + ${JSON.stringify(
|
|
17
|
-
return extractBones(container, ${JSON.stringify(
|
|
191
|
+
var container = document.querySelector(${JSON.stringify(selector)});
|
|
192
|
+
if (!container) throw new Error('CastDOM: Container not found: ' + ${JSON.stringify(selector)});
|
|
193
|
+
return extractBones(container, ${JSON.stringify(options)});
|
|
18
194
|
})()
|
|
19
|
-
`.trim()
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
195
|
+
`.trim();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/build/snapshot.ts
|
|
199
|
+
async function getPlaywright() {
|
|
200
|
+
try {
|
|
201
|
+
return await import("playwright");
|
|
202
|
+
} catch {
|
|
203
|
+
throw new Error(
|
|
204
|
+
"CastDOM: Playwright is required for extraction. Install it with:\n npm install -D playwright\n npx playwright install chromium"
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function hashSkeleton(breakpoints) {
|
|
209
|
+
const str = JSON.stringify(breakpoints);
|
|
210
|
+
let hash = 0;
|
|
211
|
+
for (let i = 0; i < str.length; i++) {
|
|
212
|
+
const chr = str.charCodeAt(i);
|
|
213
|
+
hash = (hash << 5) - hash + chr | 0;
|
|
214
|
+
}
|
|
215
|
+
return Math.abs(hash).toString(36).padStart(8, "0");
|
|
216
|
+
}
|
|
217
|
+
async function snapshot(options) {
|
|
218
|
+
const pw = await getPlaywright();
|
|
219
|
+
const breakpoints = options.breakpoints ?? DEFAULTS.breakpoints;
|
|
220
|
+
const headless = options.headless ?? true;
|
|
221
|
+
const timeout = options.timeout ?? 3e4;
|
|
222
|
+
const total = options.targets.length * breakpoints.length;
|
|
223
|
+
let current = 0;
|
|
224
|
+
const browser = await pw.chromium.launch({ headless });
|
|
225
|
+
const results = [];
|
|
226
|
+
try {
|
|
227
|
+
for (const target of options.targets) {
|
|
228
|
+
const url = target.route ? `${options.baseURL}${target.route}` : options.baseURL;
|
|
229
|
+
const breakpointData = [];
|
|
230
|
+
for (const vpWidth of breakpoints) {
|
|
231
|
+
current++;
|
|
232
|
+
options.onProgress?.(
|
|
233
|
+
`Extracting "${target.name}" at ${vpWidth}px`,
|
|
234
|
+
current,
|
|
235
|
+
total
|
|
236
|
+
);
|
|
237
|
+
const page = await browser.newPage();
|
|
238
|
+
try {
|
|
239
|
+
await page.setViewportSize({ width: vpWidth, height: 900 });
|
|
240
|
+
await page.goto(url, { waitUntil: "networkidle", timeout });
|
|
241
|
+
if (options.waitFor) {
|
|
242
|
+
await page.waitForSelector(options.waitFor, { timeout });
|
|
243
|
+
}
|
|
244
|
+
await page.waitForSelector(target.selector, { timeout });
|
|
245
|
+
const script = getExtractorScript(target.selector, {
|
|
246
|
+
contentAware: options.contentAware ?? DEFAULTS.contentAware,
|
|
247
|
+
minBoneSize: options.minBoneSize ?? DEFAULTS.minBoneSize
|
|
248
|
+
});
|
|
249
|
+
const data = await page.evaluate(script);
|
|
250
|
+
breakpointData.push(data);
|
|
251
|
+
} finally {
|
|
252
|
+
await page.close();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const skeleton = {
|
|
256
|
+
name: target.name,
|
|
257
|
+
hash: hashSkeleton(breakpointData),
|
|
258
|
+
breakpoints: breakpointData,
|
|
259
|
+
extractedAt: Date.now()
|
|
260
|
+
};
|
|
261
|
+
results.push({ target, skeleton });
|
|
262
|
+
}
|
|
263
|
+
} finally {
|
|
264
|
+
await browser.close();
|
|
265
|
+
}
|
|
266
|
+
return results;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/core/compress.ts
|
|
270
|
+
function compressBones(bp) {
|
|
271
|
+
const sorted = [...bp.bones].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
272
|
+
const data = [];
|
|
273
|
+
let prevX = 0;
|
|
274
|
+
let prevY = 0;
|
|
275
|
+
for (const bone of sorted) {
|
|
276
|
+
const x = Math.round(bone.x * 2);
|
|
277
|
+
const y = Math.round(bone.y * 2);
|
|
278
|
+
const w = Math.round(bone.w * 2);
|
|
279
|
+
const h = Math.round(bone.h * 2);
|
|
280
|
+
const r = Math.round(bone.r * 2);
|
|
281
|
+
const kind = BONE_KIND_INDEX[bone.kind ?? "block"];
|
|
282
|
+
data.push(x - prevX, y - prevY, w, h, r, kind);
|
|
283
|
+
prevX = x;
|
|
284
|
+
prevY = y;
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
v: 1,
|
|
288
|
+
vw: bp.viewport,
|
|
289
|
+
c: [
|
|
290
|
+
Math.round(bp.containerWidth * 2),
|
|
291
|
+
Math.round(bp.containerHeight * 2)
|
|
292
|
+
],
|
|
293
|
+
d: data
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function encodeBonesToBase64(compressed) {
|
|
297
|
+
const header = [compressed.v, compressed.vw, compressed.c[0], compressed.c[1]];
|
|
298
|
+
const allNums = [...header, compressed.d.length, ...compressed.d];
|
|
299
|
+
const bytes = [];
|
|
300
|
+
for (const n of allNums) {
|
|
301
|
+
const z = n >= 0 ? n * 2 : -n * 2 - 1;
|
|
302
|
+
let val = z;
|
|
303
|
+
while (val >= 128) {
|
|
304
|
+
bytes.push(val & 127 | 128);
|
|
305
|
+
val >>>= 7;
|
|
306
|
+
}
|
|
307
|
+
bytes.push(val & 127);
|
|
308
|
+
}
|
|
309
|
+
if (typeof Buffer !== "undefined") {
|
|
310
|
+
return Buffer.from(new Uint8Array(bytes)).toString("base64");
|
|
311
|
+
}
|
|
312
|
+
return btoa(String.fromCharCode(...bytes));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/core/renderer.ts
|
|
316
|
+
function resolveConfig(config) {
|
|
317
|
+
return {
|
|
318
|
+
color: config?.color ?? DEFAULTS.color,
|
|
319
|
+
shimmerColor: config?.shimmerColor ?? DEFAULTS.shimmerColor,
|
|
320
|
+
animationDuration: config?.animationDuration ?? DEFAULTS.animationDuration,
|
|
321
|
+
classPrefix: config?.classPrefix ?? DEFAULTS.classPrefix,
|
|
322
|
+
inlineStyles: config?.inlineStyles ?? DEFAULTS.inlineStyles
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function generateCSS(skeleton, config) {
|
|
326
|
+
const cfg = resolveConfig(config);
|
|
327
|
+
const { classPrefix: p, color, shimmerColor, animationDuration } = cfg;
|
|
328
|
+
const breakpoints = [...skeleton.breakpoints].sort(
|
|
329
|
+
(a, b) => a.viewport - b.viewport
|
|
330
|
+
);
|
|
331
|
+
const name = skeleton.name;
|
|
332
|
+
const parts = [];
|
|
333
|
+
parts.push(
|
|
334
|
+
`.${p}-${name} .${p}-bone{position:absolute;background:linear-gradient(90deg,${color} 25%,${shimmerColor} 50%,${color} 75%);background-size:200% 100%;animation:${p}-shimmer ${animationDuration}ms ease-in-out infinite}`
|
|
335
|
+
);
|
|
336
|
+
if (breakpoints.length > 1) {
|
|
337
|
+
for (let i = 0; i < breakpoints.length; i++) {
|
|
338
|
+
const bp = breakpoints[i];
|
|
339
|
+
const next = breakpoints[i + 1];
|
|
340
|
+
const prev = breakpoints[i - 1];
|
|
341
|
+
let query;
|
|
342
|
+
if (i === 0) {
|
|
343
|
+
const max = Math.floor((bp.viewport + next.viewport) / 2) - 1;
|
|
344
|
+
query = `@media(max-width:${max}px)`;
|
|
345
|
+
} else if (i === breakpoints.length - 1) {
|
|
346
|
+
const min = Math.floor((prev.viewport + bp.viewport) / 2);
|
|
347
|
+
query = `@media(min-width:${min}px)`;
|
|
348
|
+
} else {
|
|
349
|
+
const min = Math.floor((prev.viewport + bp.viewport) / 2);
|
|
350
|
+
const max = Math.floor((bp.viewport + next.viewport) / 2) - 1;
|
|
351
|
+
query = `@media(min-width:${min}px) and (max-width:${max}px)`;
|
|
352
|
+
}
|
|
353
|
+
parts.push(
|
|
354
|
+
`${query}{.${p}-${name} .${p}-bp-${bp.viewport}{display:block}}`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
} else if (breakpoints.length === 1) {
|
|
358
|
+
parts.push(
|
|
359
|
+
`.${p}-${name} .${p}-bp-${breakpoints[0].viewport}{display:block}`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return parts.join("\n");
|
|
363
|
+
}
|
|
364
|
+
function generateCriticalCSS(skeletons, config) {
|
|
365
|
+
const cfg = resolveConfig(config);
|
|
366
|
+
const parts = [];
|
|
367
|
+
parts.push(
|
|
368
|
+
`@keyframes ${cfg.classPrefix}-shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}`
|
|
369
|
+
);
|
|
370
|
+
parts.push(
|
|
371
|
+
`@media(prefers-reduced-motion:reduce){.${cfg.classPrefix}-bone{animation:none!important}}`
|
|
372
|
+
);
|
|
373
|
+
for (const skeleton of skeletons) {
|
|
374
|
+
parts.push(generateCSS(skeleton, config));
|
|
375
|
+
}
|
|
376
|
+
return parts.join("\n");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/build/compiler.ts
|
|
380
|
+
function deduplicateBones(skeleton, threshold) {
|
|
381
|
+
const result = {
|
|
382
|
+
...skeleton,
|
|
383
|
+
breakpoints: skeleton.breakpoints.map((bp) => {
|
|
384
|
+
const bones = [...bp.bones];
|
|
385
|
+
const keep = new Array(bones.length).fill(true);
|
|
386
|
+
for (let i = 0; i < bones.length; i++) {
|
|
387
|
+
if (!keep[i]) continue;
|
|
388
|
+
for (let j = i + 1; j < bones.length; j++) {
|
|
389
|
+
if (!keep[j]) continue;
|
|
390
|
+
const a = bones[i];
|
|
391
|
+
const b = bones[j];
|
|
392
|
+
if (b.x >= a.x - threshold && b.y >= a.y - threshold && b.x + b.w <= a.x + a.w + threshold && b.y + b.h <= a.y + a.h + threshold) {
|
|
393
|
+
if (a.w * a.h > b.w * b.h * 4) {
|
|
394
|
+
} else {
|
|
395
|
+
keep[j] = false;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (a.x >= b.x - threshold && a.y >= b.y - threshold && a.x + a.w <= b.x + b.w + threshold && a.y + a.h <= b.y + b.h + threshold) {
|
|
399
|
+
if (b.w * b.h > a.w * a.h * 4) {
|
|
400
|
+
} else {
|
|
401
|
+
keep[i] = false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
...bp,
|
|
408
|
+
bones: bones.filter((_, i) => keep[i])
|
|
409
|
+
};
|
|
410
|
+
})
|
|
411
|
+
};
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
function validateSkeleton(skeleton) {
|
|
415
|
+
return {
|
|
416
|
+
...skeleton,
|
|
417
|
+
breakpoints: skeleton.breakpoints.map((bp) => ({
|
|
418
|
+
...bp,
|
|
419
|
+
bones: bp.bones.filter(
|
|
420
|
+
(bone) => isFinite(bone.x) && isFinite(bone.y) && bone.w > 0 && bone.h > 0 && isFinite(bone.r)
|
|
421
|
+
)
|
|
422
|
+
}))
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
function compile(results, options = {}) {
|
|
426
|
+
const dedupe = options.dedupe ?? true;
|
|
427
|
+
const overlapThreshold = options.overlapThreshold ?? 2;
|
|
428
|
+
const shouldCompress = options.compress ?? true;
|
|
429
|
+
const generateTypes = options.generateTypes ?? true;
|
|
430
|
+
const base64 = options.base64 ?? false;
|
|
431
|
+
let skeletons = results.map((r) => r.skeleton);
|
|
432
|
+
skeletons = skeletons.map(validateSkeleton);
|
|
433
|
+
if (dedupe) {
|
|
434
|
+
skeletons = skeletons.map((s) => deduplicateBones(s, overlapThreshold));
|
|
435
|
+
}
|
|
436
|
+
const rawJSON = JSON.stringify(skeletons);
|
|
437
|
+
const rawSize = rawJSON.length;
|
|
438
|
+
let compressedSize = rawSize;
|
|
439
|
+
const encoded = {};
|
|
440
|
+
if (shouldCompress || base64) {
|
|
441
|
+
for (const skeleton of skeletons) {
|
|
442
|
+
const encodedBPs = [];
|
|
443
|
+
let totalCompressed = 0;
|
|
444
|
+
for (const bp of skeleton.breakpoints) {
|
|
445
|
+
const compressed = compressBones(bp);
|
|
446
|
+
totalCompressed += JSON.stringify(compressed).length;
|
|
447
|
+
if (base64) {
|
|
448
|
+
encodedBPs.push(encodeBonesToBase64(compressed));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
compressedSize = totalCompressed;
|
|
452
|
+
if (base64) {
|
|
453
|
+
encoded[skeleton.name] = encodedBPs;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const css = generateCriticalCSS(skeletons);
|
|
458
|
+
let types;
|
|
459
|
+
if (generateTypes) {
|
|
460
|
+
types = generateTypeDeclarations(skeletons);
|
|
461
|
+
}
|
|
462
|
+
const totalBones = skeletons.reduce(
|
|
463
|
+
(sum, s) => sum + s.breakpoints.reduce((bpSum, bp) => bpSum + bp.bones.length, 0),
|
|
464
|
+
0
|
|
465
|
+
);
|
|
466
|
+
const allBreakpoints = [
|
|
467
|
+
...new Set(skeletons.flatMap((s) => s.breakpoints.map((bp) => bp.viewport)))
|
|
468
|
+
].sort((a, b) => a - b);
|
|
469
|
+
const stats = {
|
|
470
|
+
skeletonCount: skeletons.length,
|
|
471
|
+
totalBones,
|
|
472
|
+
rawSize,
|
|
473
|
+
compressedSize,
|
|
474
|
+
compressionRatio: rawSize > 0 ? 1 - compressedSize / rawSize : 0,
|
|
475
|
+
breakpoints: allBreakpoints
|
|
476
|
+
};
|
|
477
|
+
return {
|
|
478
|
+
manifest: {
|
|
479
|
+
version: 1,
|
|
480
|
+
generatedAt: Date.now(),
|
|
481
|
+
skeletons
|
|
482
|
+
},
|
|
483
|
+
css,
|
|
484
|
+
types,
|
|
485
|
+
encoded: base64 ? encoded : void 0,
|
|
486
|
+
stats
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function generateTypeDeclarations(skeletons) {
|
|
490
|
+
const names = skeletons.map((s) => ` | "${s.name}"`).join("\n");
|
|
491
|
+
return `// Auto-generated by CastDOM \u2014 do not edit
|
|
24
492
|
// Regenerate with: npx castdom build
|
|
25
493
|
|
|
26
494
|
export type CastDOMSkeletonName =
|
|
27
|
-
${
|
|
28
|
-
`)};
|
|
495
|
+
${names};
|
|
29
496
|
|
|
30
|
-
declare module "
|
|
497
|
+
declare module "castdom" {
|
|
31
498
|
interface CastDOMRegistry {
|
|
32
|
-
${
|
|
33
|
-
`)}
|
|
499
|
+
${skeletons.map((s) => ` "${s.name}": true;`).join("\n")}
|
|
34
500
|
}
|
|
35
501
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
502
|
+
`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/build/codegen.ts
|
|
506
|
+
function generateFiles(skeletons, options = {}) {
|
|
507
|
+
const outDir = options.outDir ?? ".castdom";
|
|
508
|
+
const splitFiles = options.splitFiles ?? true;
|
|
509
|
+
const esm = options.esm ?? true;
|
|
510
|
+
const cjs = options.cjs ?? false;
|
|
511
|
+
const files = [];
|
|
512
|
+
const manifest = {
|
|
513
|
+
version: 1,
|
|
514
|
+
generatedAt: Date.now(),
|
|
515
|
+
skeletons
|
|
516
|
+
};
|
|
517
|
+
files.push({
|
|
518
|
+
path: `${outDir}/manifest.json`,
|
|
519
|
+
content: JSON.stringify(manifest, null, 2)
|
|
520
|
+
});
|
|
521
|
+
const css = generateCriticalCSS(skeletons, options.config);
|
|
522
|
+
files.push({
|
|
523
|
+
path: `${outDir}/castdom.css`,
|
|
524
|
+
content: css
|
|
525
|
+
});
|
|
526
|
+
if (esm) {
|
|
527
|
+
files.push({
|
|
528
|
+
path: `${outDir}/index.js`,
|
|
529
|
+
content: generateESMModule(skeletons)
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
if (cjs) {
|
|
533
|
+
files.push({
|
|
534
|
+
path: `${outDir}/index.cjs`,
|
|
535
|
+
content: generateCJSModule(skeletons)
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
files.push({
|
|
539
|
+
path: `${outDir}/index.d.ts`,
|
|
540
|
+
content: generateTypeDefs(skeletons)
|
|
541
|
+
});
|
|
542
|
+
if (splitFiles) {
|
|
543
|
+
for (const skeleton of skeletons) {
|
|
544
|
+
files.push({
|
|
545
|
+
path: `${outDir}/skeletons/${skeleton.name}.json`,
|
|
546
|
+
content: JSON.stringify(skeleton, null, 2)
|
|
547
|
+
});
|
|
548
|
+
if (esm) {
|
|
549
|
+
files.push({
|
|
550
|
+
path: `${outDir}/skeletons/${skeleton.name}.js`,
|
|
551
|
+
content: `export default ${JSON.stringify(skeleton)};
|
|
552
|
+
`
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
files.push({
|
|
558
|
+
path: `${outDir}/loader.js`,
|
|
559
|
+
content: generateLoader(skeletons)
|
|
560
|
+
});
|
|
561
|
+
files.push({
|
|
562
|
+
path: `${outDir}/nextjs-loading.tsx`,
|
|
563
|
+
content: generateNextJSLoading(skeletons)
|
|
564
|
+
});
|
|
565
|
+
return files;
|
|
566
|
+
}
|
|
567
|
+
function generateESMModule(skeletons) {
|
|
568
|
+
const imports = skeletons.map((s) => `import ${safeName(s.name)} from "./skeletons/${s.name}.json" with { type: "json" };`).join("\n");
|
|
569
|
+
const exports = skeletons.map((s) => ` "${s.name}": ${safeName(s.name)},`).join("\n");
|
|
570
|
+
return `// Auto-generated by CastDOM \u2014 do not edit
|
|
571
|
+
${imports}
|
|
41
572
|
|
|
42
573
|
export const skeletons = {
|
|
43
|
-
${
|
|
574
|
+
${exports}
|
|
44
575
|
};
|
|
45
576
|
|
|
46
577
|
export const manifest = {
|
|
@@ -49,10 +580,13 @@ export const manifest = {
|
|
|
49
580
|
};
|
|
50
581
|
|
|
51
582
|
export default manifest;
|
|
52
|
-
|
|
583
|
+
`;
|
|
584
|
+
}
|
|
585
|
+
function generateCJSModule(skeletons) {
|
|
586
|
+
return `// Auto-generated by CastDOM \u2014 do not edit
|
|
53
587
|
"use strict";
|
|
54
588
|
|
|
55
|
-
const manifest = ${JSON.stringify({version:1,skeletons
|
|
589
|
+
const manifest = ${JSON.stringify({ version: 1, skeletons }, null, 2)};
|
|
56
590
|
|
|
57
591
|
module.exports = manifest;
|
|
58
592
|
module.exports.default = manifest;
|
|
@@ -60,12 +594,15 @@ module.exports.skeletons = manifest.skeletons.reduce((acc, s) => {
|
|
|
60
594
|
acc[s.name] = s;
|
|
61
595
|
return acc;
|
|
62
596
|
}, {});
|
|
63
|
-
|
|
64
|
-
|
|
597
|
+
`;
|
|
598
|
+
}
|
|
599
|
+
function generateTypeDefs(skeletons) {
|
|
600
|
+
const names = skeletons.map((s) => ` | "${s.name}"`).join("\n");
|
|
601
|
+
return `// Auto-generated by CastDOM \u2014 do not edit
|
|
602
|
+
import type { SkeletonData } from "castdom";
|
|
65
603
|
|
|
66
604
|
export type SkeletonName =
|
|
67
|
-
${
|
|
68
|
-
`)};
|
|
605
|
+
${names};
|
|
69
606
|
|
|
70
607
|
export declare const skeletons: Record<SkeletonName, SkeletonData>;
|
|
71
608
|
|
|
@@ -75,34 +612,48 @@ export declare const manifest: {
|
|
|
75
612
|
};
|
|
76
613
|
|
|
77
614
|
export default manifest;
|
|
78
|
-
|
|
615
|
+
`;
|
|
616
|
+
}
|
|
617
|
+
function generateLoader(skeletons) {
|
|
618
|
+
return `// Auto-generated by CastDOM \u2014 do not edit
|
|
79
619
|
// Import this file once at your app's entry point to register all skeletons.
|
|
80
620
|
//
|
|
81
621
|
// import ".castdom/loader.js";
|
|
82
622
|
|
|
83
|
-
import { loadManifest } from "
|
|
623
|
+
import { loadManifest } from "castdom";
|
|
84
624
|
import manifest from "./manifest.json" with { type: "json" };
|
|
85
625
|
|
|
86
626
|
loadManifest(manifest);
|
|
87
|
-
|
|
88
|
-
|
|
627
|
+
`;
|
|
628
|
+
}
|
|
629
|
+
function generateNextJSLoading(skeletons) {
|
|
630
|
+
if (skeletons.length === 0) return "export default function Loading() { return null; }\n";
|
|
631
|
+
const first = skeletons[0];
|
|
632
|
+
return `// Auto-generated by CastDOM \u2014 do not edit
|
|
89
633
|
// Copy this to your app/loading.tsx or page-specific loading.tsx
|
|
90
634
|
//
|
|
91
635
|
// For multiple skeletons, use <CastDOM name="..."> directly.
|
|
92
636
|
|
|
93
|
-
import { CastDOM, CastDOMStyle } from "
|
|
637
|
+
import { CastDOM, CastDOMStyle } from "castdom/react";
|
|
94
638
|
|
|
95
639
|
export default function Loading() {
|
|
96
640
|
return (
|
|
97
641
|
<>
|
|
98
642
|
<CastDOMStyle />
|
|
99
|
-
<CastDOM name="${
|
|
643
|
+
<CastDOM name="${first.name}" loading={true}>
|
|
100
644
|
{null}
|
|
101
645
|
</CastDOM>
|
|
102
646
|
</>
|
|
103
647
|
);
|
|
104
648
|
}
|
|
105
|
-
|
|
649
|
+
`;
|
|
650
|
+
}
|
|
651
|
+
function safeName(name) {
|
|
652
|
+
return name.replace(/[^a-zA-Z0-9_$]/g, "_");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/cli.ts
|
|
656
|
+
var HELP = `
|
|
106
657
|
CastDOM \u2014 Pixel-perfect skeleton screens from your real DOM
|
|
107
658
|
|
|
108
659
|
Usage:
|
|
@@ -121,15 +672,228 @@ Build options:
|
|
|
121
672
|
--headless Run browser in headless mode (default: true)
|
|
122
673
|
--no-headless Run browser with visible UI (for debugging)
|
|
123
674
|
--verbose Show detailed progress output
|
|
124
|
-
`.trim()
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
675
|
+
`.trim();
|
|
676
|
+
var VERSION = "1.0.0";
|
|
677
|
+
function parseArgs(args) {
|
|
678
|
+
const opts = {
|
|
679
|
+
command: args[0] ?? "build",
|
|
680
|
+
url: "http://localhost:3000",
|
|
681
|
+
configPath: "castdom.config.json",
|
|
682
|
+
outDir: DEFAULTS.outDir,
|
|
683
|
+
breakpoints: [...DEFAULTS.breakpoints],
|
|
684
|
+
headless: true,
|
|
685
|
+
verbose: false
|
|
686
|
+
};
|
|
687
|
+
for (let i = 1; i < args.length; i++) {
|
|
688
|
+
const arg = args[i];
|
|
689
|
+
switch (arg) {
|
|
690
|
+
case "--url":
|
|
691
|
+
opts.url = args[++i] ?? opts.url;
|
|
692
|
+
break;
|
|
693
|
+
case "--config":
|
|
694
|
+
opts.configPath = args[++i] ?? opts.configPath;
|
|
695
|
+
break;
|
|
696
|
+
case "--out":
|
|
697
|
+
opts.outDir = args[++i] ?? opts.outDir;
|
|
698
|
+
break;
|
|
699
|
+
case "--breakpoints":
|
|
700
|
+
opts.breakpoints = (args[++i] ?? "").split(",").map(Number).filter((n) => n > 0);
|
|
701
|
+
break;
|
|
702
|
+
case "--headless":
|
|
703
|
+
opts.headless = true;
|
|
704
|
+
break;
|
|
705
|
+
case "--no-headless":
|
|
706
|
+
opts.headless = false;
|
|
707
|
+
break;
|
|
708
|
+
case "--verbose":
|
|
709
|
+
opts.verbose = true;
|
|
710
|
+
break;
|
|
711
|
+
case "--help":
|
|
712
|
+
case "-h":
|
|
713
|
+
console.log(HELP);
|
|
714
|
+
process.exit(0);
|
|
715
|
+
case "--version":
|
|
716
|
+
case "-v":
|
|
717
|
+
console.log(VERSION);
|
|
718
|
+
process.exit(0);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return opts;
|
|
722
|
+
}
|
|
723
|
+
function loadConfig(configPath) {
|
|
724
|
+
const absPath = resolve(configPath);
|
|
725
|
+
if (!existsSync(absPath)) return {};
|
|
726
|
+
try {
|
|
727
|
+
const raw = readFileSync(absPath, "utf-8");
|
|
728
|
+
return JSON.parse(raw);
|
|
729
|
+
} catch (err) {
|
|
730
|
+
console.error(`Failed to parse config: ${absPath}`);
|
|
731
|
+
return {};
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function log(verbose, ...args) {
|
|
735
|
+
if (verbose) console.log(...args);
|
|
736
|
+
}
|
|
737
|
+
async function cmdBuild(opts) {
|
|
738
|
+
const config = loadConfig(opts.configPath);
|
|
739
|
+
const outDir = resolve(config.outDir ?? opts.outDir);
|
|
740
|
+
const breakpoints = config.breakpoints ?? opts.breakpoints;
|
|
741
|
+
const url = config.devServer ?? opts.url;
|
|
742
|
+
const targets = config.targets ?? [];
|
|
743
|
+
if (targets.length === 0) {
|
|
744
|
+
console.log("No targets configured. Scanning for [data-castdom] elements...");
|
|
745
|
+
targets.push({
|
|
746
|
+
name: "auto",
|
|
747
|
+
selector: "[data-castdom]"
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
console.log(`
|
|
751
|
+
CastDOM Build`);
|
|
752
|
+
console.log(` Server: ${url}`);
|
|
753
|
+
console.log(` Breakpoints: ${breakpoints.join(", ")}px`);
|
|
754
|
+
console.log(` Targets: ${targets.length}`);
|
|
755
|
+
console.log(` Output: ${outDir}
|
|
756
|
+
`);
|
|
757
|
+
const startTime = Date.now();
|
|
758
|
+
const results = await snapshot({
|
|
759
|
+
baseURL: url,
|
|
760
|
+
targets,
|
|
761
|
+
breakpoints,
|
|
762
|
+
headless: opts.headless,
|
|
763
|
+
contentAware: config.contentAware ?? true,
|
|
764
|
+
minBoneSize: config.minBoneSize ?? DEFAULTS.minBoneSize,
|
|
765
|
+
onProgress(message, current, total) {
|
|
766
|
+
log(opts.verbose, ` [${current}/${total}] ${message}`);
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
const extractTime = Date.now() - startTime;
|
|
770
|
+
log(opts.verbose, `
|
|
771
|
+
Extraction: ${extractTime}ms`);
|
|
772
|
+
const compileStart = Date.now();
|
|
773
|
+
const compiled = compile(results);
|
|
774
|
+
const compileTime = Date.now() - compileStart;
|
|
775
|
+
log(opts.verbose, ` Compilation: ${compileTime}ms`);
|
|
776
|
+
const files = generateFiles(compiled.manifest.skeletons, {
|
|
777
|
+
outDir,
|
|
778
|
+
config
|
|
779
|
+
});
|
|
780
|
+
for (const file of files) {
|
|
781
|
+
const dir = file.path.substring(0, file.path.lastIndexOf("/"));
|
|
782
|
+
mkdirSync(dir, { recursive: true });
|
|
783
|
+
writeFileSync(file.path, file.content, "utf-8");
|
|
784
|
+
log(opts.verbose, ` Written: ${file.path}`);
|
|
785
|
+
}
|
|
786
|
+
const totalTime = Date.now() - startTime;
|
|
787
|
+
console.log(` Skeletons: ${compiled.stats.skeletonCount}`);
|
|
788
|
+
console.log(` Bones: ${compiled.stats.totalBones}`);
|
|
789
|
+
console.log(` Raw size: ${(compiled.stats.rawSize / 1024).toFixed(1)} KB`);
|
|
790
|
+
console.log(
|
|
791
|
+
` Compressed: ${(compiled.stats.compressedSize / 1024).toFixed(1)} KB (${(compiled.stats.compressionRatio * 100).toFixed(0)}% smaller)`
|
|
792
|
+
);
|
|
793
|
+
console.log(` Files: ${files.length}`);
|
|
794
|
+
console.log(` Total time: ${totalTime}ms`);
|
|
795
|
+
console.log(`
|
|
128
796
|
Done. Import the loader to register:
|
|
129
|
-
`)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
797
|
+
`);
|
|
798
|
+
console.log(` import "${outDir}/loader.js";
|
|
799
|
+
`);
|
|
800
|
+
}
|
|
801
|
+
function cmdInit(opts) {
|
|
802
|
+
const configPath = resolve(opts.configPath);
|
|
803
|
+
if (existsSync(configPath)) {
|
|
804
|
+
console.log(`Config already exists: ${configPath}`);
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const template = {
|
|
808
|
+
devServer: "http://localhost:3000",
|
|
809
|
+
outDir: ".castdom",
|
|
810
|
+
breakpoints: [375, 768, 1280],
|
|
811
|
+
color: "#e0e0e0",
|
|
812
|
+
shimmerColor: "#f0f0f0",
|
|
813
|
+
animationDuration: 1500,
|
|
814
|
+
contentAware: true,
|
|
815
|
+
minBoneSize: 4,
|
|
816
|
+
targets: [
|
|
817
|
+
{
|
|
818
|
+
name: "example-card",
|
|
819
|
+
selector: '[data-castdom="example-card"]',
|
|
820
|
+
route: "/"
|
|
821
|
+
}
|
|
822
|
+
]
|
|
823
|
+
};
|
|
824
|
+
writeFileSync(configPath, JSON.stringify(template, null, 2) + "\n", "utf-8");
|
|
825
|
+
console.log(`Created: ${configPath}`);
|
|
826
|
+
console.log(`
|
|
827
|
+
Edit the "targets" array to define your skeleton targets.`);
|
|
828
|
+
console.log(`Then run: npx castdom build
|
|
829
|
+
`);
|
|
830
|
+
}
|
|
831
|
+
function cmdList(opts) {
|
|
832
|
+
const config = loadConfig(opts.configPath);
|
|
833
|
+
const outDir = resolve(config.outDir ?? opts.outDir);
|
|
834
|
+
const manifestPath = join(outDir, "manifest.json");
|
|
835
|
+
if (!existsSync(manifestPath)) {
|
|
836
|
+
console.log(`No manifest found. Run "npx castdom build" first.`);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
840
|
+
const skeletons = manifest.skeletons ?? [];
|
|
841
|
+
console.log(`
|
|
842
|
+
CastDOM Skeletons (${skeletons.length}):
|
|
843
|
+
`);
|
|
844
|
+
for (const s of skeletons) {
|
|
845
|
+
const boneCount = s.breakpoints.reduce(
|
|
846
|
+
(sum, bp) => sum + bp.bones.length,
|
|
847
|
+
0
|
|
848
|
+
);
|
|
849
|
+
const bps = s.breakpoints.map((bp) => `${bp.viewport}px`).join(", ");
|
|
850
|
+
console.log(` ${s.name}`);
|
|
851
|
+
console.log(` Bones: ${boneCount} | Breakpoints: ${bps} | Hash: ${s.hash}`);
|
|
852
|
+
}
|
|
853
|
+
console.log();
|
|
854
|
+
}
|
|
855
|
+
function cmdClean(opts) {
|
|
856
|
+
const config = loadConfig(opts.configPath);
|
|
857
|
+
const outDir = resolve(config.outDir ?? opts.outDir);
|
|
858
|
+
if (!existsSync(outDir)) {
|
|
859
|
+
console.log("Nothing to clean.");
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const { rmSync } = __require("fs");
|
|
863
|
+
rmSync(outDir, { recursive: true, force: true });
|
|
864
|
+
console.log(`Removed: ${outDir}`);
|
|
865
|
+
}
|
|
866
|
+
async function main() {
|
|
867
|
+
const args = process.argv.slice(2);
|
|
868
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
869
|
+
console.log(HELP);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
873
|
+
console.log(VERSION);
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const opts = parseArgs(args);
|
|
877
|
+
switch (opts.command) {
|
|
878
|
+
case "build":
|
|
879
|
+
await cmdBuild(opts);
|
|
880
|
+
break;
|
|
881
|
+
case "init":
|
|
882
|
+
cmdInit(opts);
|
|
883
|
+
break;
|
|
884
|
+
case "list":
|
|
885
|
+
cmdList(opts);
|
|
886
|
+
break;
|
|
887
|
+
case "clean":
|
|
888
|
+
cmdClean(opts);
|
|
889
|
+
break;
|
|
890
|
+
default:
|
|
891
|
+
console.error(`Unknown command: ${opts.command}`);
|
|
892
|
+
console.log(HELP);
|
|
893
|
+
process.exit(1);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
main().catch((err) => {
|
|
897
|
+
console.error("CastDOM error:", err.message ?? err);
|
|
898
|
+
process.exit(1);
|
|
899
|
+
});
|