Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

147 lines
3.2 KiB
TypeScript

import { unzipSync } from "fflate";
export type FileEntry = {
path: string;
name: string;
size: number;
isText: boolean;
};
export type FileTreeNode = {
name: string;
path: string;
type: "file" | "directory";
size?: number;
children?: FileTreeNode[];
};
export type VirtualArchive = {
entries: FileEntry[];
tree: FileTreeNode[];
fileCount: number;
totalBytes: number;
readText(path: string): string | null;
isText(path: string): boolean;
};
const TEXT_EXTENSIONS = new Set([
"ts",
"tsx",
"js",
"jsx",
"mjs",
"cjs",
"json",
"md",
"mdx",
"txt",
"yml",
"yaml",
"css",
"scss",
"html",
"xml",
"svg",
"env",
"example",
"gitignore",
"mts",
"cts",
"toml",
]);
function isTextFile(name: string): boolean {
if (name.startsWith(".")) return true;
const ext = name.includes(".") ? name.split(".").pop()!.toLowerCase() : "";
return TEXT_EXTENSIONS.has(ext);
}
export function createVirtualArchive(zipBytes: Uint8Array): VirtualArchive {
const unzipped = unzipSync(zipBytes);
const decoder = new TextDecoder();
const fileMap = new Map<string, Uint8Array>();
const entries: FileEntry[] = [];
for (const [rawPath, data] of Object.entries(unzipped)) {
const path = rawPath.replace(/^\/+/, "");
if (path.endsWith("/")) continue;
const name = path.split("/").pop()!;
const text = isTextFile(name);
fileMap.set(path, data);
entries.push({ path, name, size: data.length, isText: text });
}
entries.sort((a, b) => a.path.localeCompare(b.path));
return {
entries,
tree: buildTree(entries),
fileCount: entries.length,
totalBytes: entries.reduce((sum, e) => sum + e.size, 0),
readText(path: string) {
const data = fileMap.get(path);
if (!data) return null;
return decoder.decode(data);
},
isText(path: string) {
return entries.find((e) => e.path === path)?.isText ?? false;
},
};
}
function buildTree(entries: FileEntry[]): FileTreeNode[] {
const root: FileTreeNode = {
name: "",
path: "",
type: "directory",
children: [],
};
for (const entry of entries) {
const parts = entry.path.split("/");
let current = root;
let currentPath = "";
for (let i = 0; i < parts.length; i++) {
const part = parts[i]!;
currentPath = currentPath ? `${currentPath}/${part}` : part;
if (i === parts.length - 1) {
current.children!.push({
name: part,
path: currentPath,
type: "file",
size: entry.size,
});
} else {
let dir = current.children!.find(
(c) => c.type === "directory" && c.name === part,
);
if (!dir) {
dir = {
name: part,
path: currentPath,
type: "directory",
children: [],
};
current.children!.push(dir);
}
current = dir;
}
}
}
sortChildren(root);
return root.children ?? [];
}
function sortChildren(node: FileTreeNode) {
if (!node.children) return;
node.children.sort((a, b) => {
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
return a.name.localeCompare(b.name);
});
node.children.forEach(sortChildren);
}