Skip to content

Instantly share code, notes, and snippets.

@Zxilly
Created March 24, 2026 17:51
Show Gist options
  • Select an option

  • Save Zxilly/1f219019e2cc4c1777a44065f88b8cf1 to your computer and use it in GitHub Desktop.

Select an option

Save Zxilly/1f219019e2cc4c1777a44065f88b8cf1 to your computer and use it in GitHub Desktop.
import https from "https";
import { parse as parseHtml } from "node-html-parser";
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
import type { Node, ObjectProperty } from "@babel/types";
import { z } from "zod";
const TARGET_PAGE = "https://cangjie-lang.cn/download/1.1.0-beta.24";
// ─── Schema 定义(同时承担类型和运行时校验) ─────────────────────────────────
// version.js 内部结构
const RawPackageSchema = z.object({
name: z.string(),
url: z.string(),
sha: z.string().default(""),
version: z.string(),
time: z.string(),
size: z.string().optional(),
});
const RawVersionDataSchema = z.object({
time: z.string(),
name: z.string().nullable(),
version: z.string(),
versiontype: z.enum(["LTS", "STS"]),
list: z.array(
z.object({
name: z.string().nullable(),
list: z.array(
z.object({
name: z.string(),
list: z.array(RawPackageSchema),
})
),
})
),
});
const VersionMapSchema = z.record(z.string(), RawVersionDataSchema);
// 输出结构
const PlatformSchema = z.enum(["win32-x64", "darwin-arm64", "darwin-x64", "linux-arm64", "linux-x64"]);
const SdkPackageSchema = z.object({
name: z.string(),
sha256: z.string(),
url: z.string(),
});
// 推导类型,不再手写 interface
type Platform = z.infer<typeof PlatformSchema>;
type SdkPackage = z.infer<typeof SdkPackageSchema>;
type VersionPackages = Partial<Record<Platform, SdkPackage>>;
type RawVersionData = z.infer<typeof RawVersionDataSchema>;
// ─── 工具函数 ─────────────────────────────────────────────────────────────────
function fetchUrl(url: string): Promise<string> {
return new Promise((resolve, reject) => {
https
.get(url, { headers: { "User-Agent": "Mozilla/5.0" } }, (res) => {
if (
res.statusCode != null &&
res.statusCode >= 300 &&
res.statusCode < 400 &&
res.headers.location
) {
return fetchUrl(res.headers.location).then(resolve).catch(reject);
}
let data = "";
res.on("data", (c: Buffer) => (data += c));
res.on("end", () => resolve(data));
})
.on("error", reject);
});
}
// ─── Step 1: 直接从页面 HTML 提取 version.js URL ─────────────────────────────
async function getVersionJsUrl(): Promise<string> {
console.error("[1] 拉取主页 HTML,提取 version.js URL...");
const html = await fetchUrl(TARGET_PAGE);
const doc = parseHtml(html);
const src = doc
.querySelectorAll("script[src]")
.map((el) => el.getAttribute("src")!)
.find((s) => /version.*\.js($|\?)/.test(s));
if (!src) throw new Error("未在 HTML 中找到 version.js script 标签");
return src.startsWith("//") ? "https:" + src : src;
}
// ─── Step 2: 拉取 version.js ─────────────────────────────────────────────────
async function fetchVersionJs(url: string): Promise<string> {
console.error("[2] 拉取 version.js:", url);
return fetchUrl(url);
}
// ─── Step 3: Babel AST 解析所有版本 ──────────────────────────────────────────
// 将 AST 节点转换为 JS 值,函数调用(如翻译函数 t("page.xxx"))返回 null
function nodeToValue(node: Node | null | undefined): unknown {
if (!node) return null;
switch (node.type) {
case "StringLiteral": return node.value;
case "NumericLiteral": return node.value;
case "BooleanLiteral": return node.value;
case "NullLiteral": return null;
case "TemplateLiteral": return node.quasis.map((q) => q.value.raw).join("...");
case "ArrayExpression": return node.elements.map((el) => nodeToValue(el));
case "ObjectExpression": {
const obj: Record<string, unknown> = {};
for (const prop of node.properties) {
if (prop.type !== "ObjectProperty") continue;
const p = prop as ObjectProperty;
const key =
p.key.type === "StringLiteral"
? p.key.value
: p.key.type === "Identifier"
? p.key.name
: String((p.key as any).value);
obj[key] = nodeToValue(p.value as Node);
}
return obj;
}
case "CallExpression": // t("page.xxx") 等翻译函数调用 → 忽略
case "UnaryExpression": return null;
default: return undefined;
}
}
function detectPlatform(filename: string): Platform | null {
const n = filename.toLowerCase();
if (n.includes("windows")) return "win32-x64";
if (n.includes("mac-aarch64") || n.includes("darwin_aarch64")) return "darwin-arm64";
if (n.includes("mac-x64") || n.includes("darwin_x64")) return "darwin-x64";
if (n.includes("linux-aarch64") || n.includes("linux_aarch64")) return "linux-arm64";
if (n.includes("linux-x64") || n.includes("linux_x64")) return "linux-x64";
return null;
}
function extractAllVersions(jsContent: string) {
console.error("[3] Babel AST 解析所有版本数据...");
const ast = parse(jsContent, { sourceType: "script", errorRecovery: true });
let versionMap: z.infer<typeof VersionMapSchema> | null = null;
traverse(ast, {
VariableDeclarator(path) {
const init = path.node.init;
if (init?.type !== "ObjectExpression") return;
const firstProp = init.properties[0];
if (firstProp?.type !== "ObjectProperty") return;
const firstKey =
firstProp.key.type === "StringLiteral"
? firstProp.key.value
: firstProp.key.type === "Identifier"
? firstProp.key.name
: "";
if (/^\d+\.\d+/.test(firstKey)) {
versionMap = VersionMapSchema.parse(nodeToValue(init));
path.stop();
}
},
});
if (!versionMap) throw new Error("未找到版本数据对象");
console.error(" → 共找到版本:", Object.keys(versionMap).join(", "));
const stsVersions: Record<string, VersionPackages> = {};
const ltsVersions: Record<string, VersionPackages> = {};
for (const [version, data] of Object.entries(versionMap)) {
const packages: VersionPackages = {};
for (const group of data.list ?? []) {
for (const platformGroup of group.list ?? []) {
for (const pkg of platformGroup.list ?? []) {
if (!pkg?.name || pkg.name.endsWith(".exe")) continue;
const platform = detectPlatform(pkg.name);
if (platform) {
packages[platform] = {
name: pkg.name,
sha256: pkg.sha ?? "",
url: pkg.url?.startsWith("http")
? pkg.url
: `https://cangjie-lang.cn${pkg.url}`,
};
}
}
}
}
if (Object.keys(packages).length > 0) {
(data.versiontype === "LTS" ? ltsVersions : stsVersions)[version] = packages;
}
}
return {
channels: {
sts: { versions: stsVersions, latest: Object.keys(stsVersions).sort().at(-1) ?? null },
lts: { versions: ltsVersions, latest: Object.keys(ltsVersions).sort().at(-1) ?? null },
},
};
}
// ─── Main ─────────────────────────────────────────────────────────────────────
async function main(): Promise<void> {
const versionJsUrl = await getVersionJsUrl();
console.error(" → 找到:", versionJsUrl);
const jsContent = await fetchVersionJs(versionJsUrl);
console.error(" → JS 大小:", jsContent.length, "bytes");
const sdkVersions = extractAllVersions(jsContent);
console.error(" → STS 版本数:", Object.keys(sdkVersions.channels.sts.versions).length);
console.error(" → LTS 版本数:", Object.keys(sdkVersions.channels.lts.versions).length);
console.log(JSON.stringify(sdkVersions, null, 2));
}
main().catch((e: Error) => {
console.error("Fatal error:", e.message);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment