Files
wehub-resource-sync 9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:48 +08:00

428 lines
15 KiB
JavaScript

import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import * as path from 'node:path';
export const UIVERSE_BASE_URL = 'https://uiverse.io';
const ROUTE_DATA_KEY = 'routes/$username.$friendlyId';
const CODE_DATA_KEY = 'routes/resource.post.code.$id';
const EXPORT_TARGET_BUTTON_LABELS = ['React', 'Vue', 'Svelte', 'Lit'];
function trimPathSegment(value) {
return String(value || '').trim().replace(/^\/+|\/+$/g, '');
}
export function parseComponentInput(input) {
const raw = String(input || '').trim();
if (!raw) {
throw new Error('Missing component input. Pass a full Uiverse URL or an author/slug identifier.');
}
let pathname = raw;
if (/^https?:\/\//i.test(raw)) {
const url = new URL(raw);
if (url.hostname !== 'uiverse.io' && url.hostname !== 'www.uiverse.io') {
throw new Error(`Unsupported non-Uiverse URL: ${raw}`);
}
pathname = url.pathname;
}
const cleaned = trimPathSegment(pathname);
const segments = cleaned.split('/').filter(Boolean);
if (segments.length !== 2) {
throw new Error(`Could not parse author/slug from input: ${raw}`);
}
const [username, slug] = segments;
if (!username || !slug) {
throw new Error(`Invalid component identifier: ${raw}. Expected author/slug.`);
}
return {
raw,
username,
slug,
url: `${UIVERSE_BASE_URL}/${username}/${slug}`,
};
}
async function fetchJsonInBrowser(page, url) {
const raw = await page.evaluate(`(async () => {
const url = ${JSON.stringify(url)};
const response = await fetch(url, {
credentials: 'include',
headers: {
accept: 'application/json, text/plain, */*',
},
});
const text = await response.text();
return JSON.stringify({
ok: response.ok,
status: response.status,
statusText: response.statusText,
text,
url,
});
})()`);
const result = JSON.parse(raw);
if (!result?.ok) {
throw new Error(`Request failed: ${result?.status} ${result?.statusText} (${result?.url || url})`);
}
try {
return JSON.parse(result.text);
} catch {
throw new Error(`Response was not valid JSON: ${url}`);
}
}
export async function getPostDetails(page, input) {
const normalized = parseComponentInput(input);
await page.goto(normalized.url);
const raw = await page.evaluate(`(async () => {
const key = ${JSON.stringify(ROUTE_DATA_KEY)};
const loaderData = window.__remixContext?.state?.loaderData || {};
const routeData = loaderData[key];
return JSON.stringify({ routeData: routeData || null, keys: Object.keys(loaderData) });
})()`);
const parsed = JSON.parse(raw);
let routeData = parsed?.routeData;
if (!routeData?.post?.id) {
const routeUrl = `${normalized.url}?_data=${encodeURIComponent(ROUTE_DATA_KEY)}`;
routeData = await fetchJsonInBrowser(page, routeUrl);
}
if (!routeData?.post?.id) {
throw new Error(`Could not resolve post.id from the component page: ${normalized.url}`);
}
return {
...normalized,
post: routeData.post,
routeData,
};
}
export async function getRawCode(page, postId) {
const codeUrl = `${UIVERSE_BASE_URL}/resource/post/code/${postId}?v=1&_data=${encodeURIComponent(CODE_DATA_KEY)}`;
const payload = await fetchJsonInBrowser(page, codeUrl);
if (typeof payload?.html !== 'string' || typeof payload?.css !== 'string') {
throw new Error(`Unexpected code payload shape: ${codeUrl}`);
}
return payload;
}
export function inferLanguage(target, post) {
if (target === 'react') return 'tsx';
if (target === 'vue') return 'vue';
if (target === 'html') return post?.isTailwind ? 'html+tailwind' : 'html';
if (target === 'css') return 'css';
return 'text';
}
export function getCodeLength(code) {
return String(code || '').length;
}
function normalizeExportTarget(target) {
return String(target || '').trim().toLowerCase() === 'vue' ? 'Vue' : 'React';
}
export async function extractExportCode(page, target = 'react') {
const targetLabel = normalizeExportTarget(target);
const raw = await page.evaluate(`(async () => {
const targetLabel = ${JSON.stringify(targetLabel)};
const exportButtonLabel = 'Export';
const exportTargetButtonLabels = ${JSON.stringify(EXPORT_TARGET_BUTTON_LABELS)};
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const triggerClick = (element) => {
if (!element) return;
element.focus?.();
const pointer = { bubbles: true, cancelable: true, composed: true, view: window };
const mouse = { bubbles: true, cancelable: true, composed: true, view: window, button: 0, buttons: 1 };
element.dispatchEvent(new PointerEvent('pointerdown', pointer));
element.dispatchEvent(new MouseEvent('mousedown', mouse));
element.dispatchEvent(new PointerEvent('pointerup', pointer));
element.dispatchEvent(new MouseEvent('mouseup', mouse));
element.dispatchEvent(new MouseEvent('click', mouse));
};
const isCompleteExportCode = (code) => {
if (!code) return false;
if (targetLabel === 'Vue') {
return code.includes('<template>')
&& code.includes('</template>')
&& code.includes('<style')
&& code.includes('</style>');
}
return code.includes('export default')
&& (code.includes('styled-components') || code.includes('StyledWrapper') || code.includes('styled.'));
};
const readCode = () => {
const dialog = document.querySelector('[role="dialog"]');
if (!dialog) return null;
const heading = dialog.querySelector('h1,h2,h3,h4,h5,h6');
if (heading && (heading.textContent || '').trim() !== targetLabel) return null;
const textarea = dialog.querySelector('textarea');
if (textarea && textarea.value) return textarea.value;
return null;
};
const exportButton = [...document.querySelectorAll('button')].find((element) => (element.textContent || '').trim() === exportButtonLabel);
const currentTargetButton = [...document.querySelectorAll('button')].find((element) => {
const text = (element.textContent || '').trim();
return exportTargetButtonLabels.includes(text);
});
const existing = readCode();
if (!existing && (!exportButton || !currentTargetButton)) {
return JSON.stringify({ ok: false, error: 'Could not find the export controls on the page.' });
}
if (!existing) {
const currentLabel = (currentTargetButton.textContent || '').trim();
if (currentLabel === targetLabel) {
triggerClick(exportButton);
} else {
triggerClick(currentTargetButton);
let menuItem = null;
for (let index = 0; index < 20; index += 1) {
menuItem = [...document.querySelectorAll('[role="menuitem"]')].find((element) => (element.textContent || '').trim() === targetLabel);
if (menuItem) break;
await sleep(100);
}
if (!menuItem) {
return JSON.stringify({ ok: false, error: 'Could not find target in export menu: ' + targetLabel });
}
triggerClick(menuItem);
}
}
let longest = existing || '';
let longestLooksComplete = isCompleteExportCode(longest);
let stableCount = 0;
for (let index = 0; index < 40; index += 1) {
await sleep(200);
const code = readCode();
if (!code) continue;
if (code.length > longest.length) {
longest = code;
longestLooksComplete = isCompleteExportCode(code);
stableCount = 0;
continue;
}
if (code === longest) {
if (longestLooksComplete) {
stableCount += 1;
if (stableCount >= 2) {
return JSON.stringify({ ok: true, code: longest, length: longest.length });
}
} else {
stableCount = 0;
}
}
}
const dialog = document.querySelector('[role="dialog"]');
if (longest && longestLooksComplete) {
return JSON.stringify({ ok: true, code: longest, length: longest.length, fallback: true });
}
return JSON.stringify({
ok: false,
error: dialog
? (targetLabel + ' dialog appeared, but the exported code never reached a stable complete state.')
: (targetLabel + ' export dialog did not appear after clicking the export controls.'),
dialogFound: Boolean(dialog),
dialogText: dialog ? (dialog.innerText || '').slice(0, 200) : null,
longestLength: longest.length,
});
})()`);
const data = JSON.parse(raw);
if (!data?.ok || typeof data.code !== 'string') {
throw new Error(data?.error || `Failed to extract ${targetLabel} export code.`);
}
return data.code;
}
export function parseHtmlRootSignature(html) {
const source = String(html || '').trim();
const match = source.match(/^<([a-zA-Z0-9-]+)([^>]*)>/);
if (!match) {
return { tag: null, id: null, classes: [] };
}
const [, tag, attrs] = match;
const idMatch = attrs.match(/\sid=["']([^"']+)["']/i);
const classMatch = attrs.match(/\sclass=["']([^"']+)["']/i);
const classes = classMatch ? classMatch[1].split(/\s+/).filter(Boolean) : [];
return {
tag: tag.toLowerCase(),
id: idMatch ? idMatch[1] : null,
classes,
};
}
export function getPreviewFallbackTags(signature) {
const tag = String(signature?.tag || '').toLowerCase();
if (!tag) return ['label', 'button', 'a', 'div'];
if (tag === 'input') return ['input', 'label', 'button', 'a', 'div'];
return [tag];
}
export async function locatePreviewElement(page, html) {
const signature = parseHtmlRootSignature(html);
const raw = await page.evaluate(`(async () => {
const sig = ${JSON.stringify(signature)};
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const fallbackTags = ${JSON.stringify(getPreviewFallbackTags(signature))};
const getSearchRoots = () => {
const roots = [document];
const seen = new Set([document]);
for (let index = 0; index < roots.length; index += 1) {
const root = roots[index];
const nodes = root.querySelectorAll ? Array.from(root.querySelectorAll('*')) : [];
for (const node of nodes) {
const shadowRoot = node.shadowRoot;
if (shadowRoot && !seen.has(shadowRoot)) {
seen.add(shadowRoot);
roots.push(shadowRoot);
}
}
}
return roots;
};
const queryAcrossRoots = (selector, limit = 200) => {
const results = [];
const seen = new Set();
for (const root of getSearchRoots()) {
const matches = root.querySelectorAll ? Array.from(root.querySelectorAll(selector)) : [];
for (const match of matches) {
if (seen.has(match)) continue;
seen.add(match);
results.push(match);
if (results.length >= limit) return results;
}
}
return results;
};
const isVisible = (element) => {
if (!element || !(element instanceof Element)) return false;
if (element.closest('[role="dialog"]')) return false;
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const scoreCandidate = (element, source) => {
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
const centerX = rect.x + rect.width / 2;
const centerY = rect.y + rect.height / 2;
const area = rect.width * rect.height;
let score = 0;
if (sig.tag && element.tagName.toLowerCase() === sig.tag) score += 30;
if (sig.id && element.id === sig.id) score += 120;
if (sig.classes.length && sig.classes.every((className) => element.classList.contains(className))) score += 120;
if (sig.tag === 'input' && source === 'shadow-host') score += 220;
if (sig.tag === 'input' && element.tagName.toLowerCase() === 'label') score += 80;
if (element.id && element.id.includes('shadow-root')) score += 80;
if (centerX <= viewportWidth * 0.65) score += 40;
if (centerY <= viewportHeight * 0.6) score += 40;
if (area <= viewportWidth * viewportHeight * 0.2) score += 30;
if (area <= viewportWidth * viewportHeight * 0.05) score += 20;
if (style.position === 'fixed') score -= 180;
if (rect.height <= 24 && rect.width >= viewportWidth * 0.8) score -= 120;
return {
source,
tag: element.tagName.toLowerCase(),
className: element.className || '',
id: element.id || '',
score,
rect: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
},
};
};
const candidates = [];
const seen = new Set();
const collect = (element, source) => {
if (!isVisible(element)) return;
if (seen.has(element)) return;
seen.add(element);
candidates.push(scoreCandidate(element, source));
};
if (sig.id) {
for (const element of queryAcrossRoots('#' + CSS.escape(sig.id), 20)) {
collect(element, 'id');
}
}
if (sig.classes.length) {
const classSelector = '.' + sig.classes.map((className) => CSS.escape(className)).join('.');
for (const element of queryAcrossRoots(classSelector, 20)) {
collect(element, 'classes');
}
}
if (sig.tag === 'input') {
for (const element of queryAcrossRoots('#shadow-root-div-ready,[id*="shadow-root"]', 20)) {
collect(element, 'shadow-host');
}
}
for (const tagName of fallbackTags) {
const tagNodes = queryAcrossRoots(tagName, 200);
for (const node of tagNodes.slice(0, 200)) {
collect(node, 'tag:' + tagName);
}
}
candidates.sort((left, right) => {
if (right.score !== left.score) return right.score - left.score;
if (left.rect.y !== right.rect.y) return left.rect.y - right.rect.y;
if (left.rect.x !== right.rect.x) return left.rect.x - right.rect.x;
return (left.rect.width * left.rect.height) - (right.rect.width * right.rect.height);
});
return JSON.stringify({ signature: sig, best: candidates[0] || null, candidates: candidates.slice(0, 5) });
})()`);
const result = JSON.parse(raw);
if (!result?.best?.rect?.width || !result?.best?.rect?.height) {
throw new Error(`Could not locate a Uiverse preview element. Candidate data: ${JSON.stringify(result)}`);
}
return result;
}
export function getDefaultOutputPath({ username, slug, suffix, extension }) {
const safeUsername = trimPathSegment(username).replace(/[^a-zA-Z0-9-_]/g, '-');
const safeSlug = trimPathSegment(slug).replace(/[^a-zA-Z0-9-_]/g, '-');
return path.join(os.tmpdir(), `opencli-uiverse-${safeUsername}-${safeSlug}-${suffix}.${extension}`);
}
export async function saveBase64File(base64, outputPath) {
const resolved = path.resolve(outputPath);
await fs.mkdir(path.dirname(resolved), { recursive: true });
await fs.writeFile(resolved, Buffer.from(base64, 'base64'));
return resolved;
}