From c4cd1624dbaac944f6b75b4dcfd7c2cc63bbde7a Mon Sep 17 00:00:00 2001 From: Kevin Z Date: Thu, 18 Jan 2024 22:00:44 -0700 Subject: [PATCH] add MCLA loading progress (#190) * add MCLA loading progress fix most tsc problems resolve Vue warn "Missing required prop *" * ignore TS2353 so far, wait for T-miracle/vitepress-plugin-comment-with-giscus#10 * format code --- .eslintrc | 3 - .github/workflows/pr-check.yml | 2 +- docs/.vitepress/analyzers/mcla.ts | 83 +++-- docs/.vitepress/auth/github.ts | 4 +- docs/.vitepress/config.ts | 4 +- docs/.vitepress/theme/components/Analyzer.vue | 129 ++++--- .../theme/components/AuthRedirect.vue | 2 +- .../.vitepress/theme/components/LogViewer.vue | 37 +- .../theme/components/TransitionExpand.vue | 24 +- .../components/TransitionExpandGroup.vue | 26 +- docs/.vitepress/theme/index.ts | 8 +- docs/.vitepress/utils/progressFetch.ts | 41 +++ docs/.vitepress/workers/mcla.worker.ts | 320 ++++++++++++++++++ docs/public/scripts/mcla_worker.js | 227 ------------- docs/server/mods.md | 9 - package.json | 4 +- pnpm-lock.yaml | 57 +++- tsconfig.json | 28 +- tsconfig.worker.json | 29 ++ 19 files changed, 658 insertions(+), 379 deletions(-) create mode 100644 docs/.vitepress/utils/progressFetch.ts create mode 100644 docs/.vitepress/workers/mcla.worker.ts delete mode 100644 docs/public/scripts/mcla_worker.js create mode 100644 tsconfig.worker.json diff --git a/.eslintrc b/.eslintrc index f9bb9964..7227caed 100644 --- a/.eslintrc +++ b/.eslintrc @@ -25,8 +25,5 @@ }, "env": { "browser": true - }, - "globals": { - "umami": "readonly" } } diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 1fbbaf0f..56ecaee2 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -1,4 +1,4 @@ -name: GitHub CI Check for PR +name: GitHub Pages for PR on: [pull_request] diff --git a/docs/.vitepress/analyzers/mcla.ts b/docs/.vitepress/analyzers/mcla.ts index b25c4e80..c4a01226 100644 --- a/docs/.vitepress/analyzers/mcla.ts +++ b/docs/.vitepress/analyzers/mcla.ts @@ -1,3 +1,4 @@ +import type { Ref } from "vue" import { useCDN } from "../cdn" export { @@ -112,9 +113,10 @@ type promiseSolver = (res: any) => void class MCLAWorker implements MCLAAPI { private readonly worker: Worker - private _version: string + private _version?: string private pendings: Map private readonly registry: FinalizationRegistry + private loadProgressWatcher?: (percent: number) => void constructor(worker: Worker) { this.worker = worker @@ -136,7 +138,7 @@ class MCLAWorker implements MCLAAPI { } get version(): string { - return this._version + return this._version || "" } private ask(data: any): Promise { @@ -149,20 +151,30 @@ class MCLAWorker implements MCLAAPI { }) this.worker.postMessage({ ...data, - _id: i, + __id: i, }) return p } - private onmsg(event) { + private onmsg(event: MessageEvent) { const { data } = event - const re = this.pendings.get(data._id) - if (re) { - this.pendings.delete(data._id) - if (data._error) { - re[1](data._error) - } else { - re[0](data) + switch (data.type) { + case "reply": { + const re = this.pendings.get(data.id) + if (re) { + this.pendings.delete(data.id) + if (data.error) { + re[1](data.error) + } else { + re[0](data) + } + } + break + } + case "load-progress": { + if (this.loadProgressWatcher) { + this.loadProgressWatcher(data.percent as number) + } } } } @@ -195,7 +207,7 @@ class MCLAWorker implements MCLAAPI { return obj } if (res.__worker_function) { - const fn = async (...args): Promise => { + const fn = async (...args: any[]): Promise => { return ( await this.ask({ type: "callObj", @@ -207,7 +219,7 @@ class MCLAWorker implements MCLAAPI { this.registry.register(fn, res.ptr) return fn } - const obj = new Object() + const obj: { [key: string | symbol]: any } = {} for (const k of Reflect.ownKeys(res)) { obj[k] = this.unwrapObj(res[k]) } @@ -217,7 +229,7 @@ class MCLAWorker implements MCLAAPI { throw new Error("Unexpected type of res: " + typeof res) } - private async call(name: string, ...args): Promise { + private async call(name: string, ...args: any[]): Promise { return this.unwrapObj( ( await this.ask({ @@ -233,6 +245,12 @@ class MCLAWorker implements MCLAAPI { this.call("release") } + private watchLoadProgress(loadProgress: Ref): void { + this.loadProgressWatcher = (percent: number) => { + loadProgress.value = percent + } + } + parseCrashReport(log: readable): Promise { return this.call("parseCrashReport", log) } @@ -253,19 +271,28 @@ class MCLAWorker implements MCLAAPI { return iterator } - static async createFromWorker(worker: Worker): Promise { + static async createFromWorker( + worker: Worker, + loadProgress?: Ref, + ): Promise { const w = new MCLAWorker(worker) + console.log("Loading MCLA in worker ...") + if (loadProgress) { + w.watchLoadProgress(loadProgress) + } await w.init() return w } } -async function loadMCLAWorker(): Promise { - return MCLAWorker.createFromWorker( - new Worker("/scripts/mcla_worker.js", { +async function loadMCLAWorker(loadProgress?: Ref): Promise { + const worker = MCLAWorker.createFromWorker( + new Worker(new URL("../workers/mcla.worker.ts", import.meta.url), { type: "classic", }), + loadProgress, ) + return worker } interface containsGoCls { @@ -276,15 +303,17 @@ interface containsMCLAIns { MCLA: MCLAAPI | undefined } -async function loadMCLA(): Promise { +async function loadMCLA(loadProgress?: Ref): Promise { if (window.Worker) { try { - return loadMCLAWorker() + return loadMCLAWorker(loadProgress) } catch (e) { // if cannot load by worker, try load inside the window } } + console.log("Loading MCLA ...") + await import(GO_WASM_EXEC_URL /* @vite-ignore */) // set variable `window.Go` const go = new (window as any as containsGoCls).Go() @@ -301,13 +330,11 @@ async function loadMCLA(): Promise { } go.run(res.instance) // the global variable MCLA cannot be defined instantly, so we have to poll it - function waitMCLA(): Promise { - if ((window as any as containsMCLAIns).MCLA) { - return - } - return new Promise((re) => setTimeout(re, 10)) // sleep 10ms - .then(waitMCLA) + const waitMCLA = (): MCLAAPI | Promise => { + return ( + (window as any as containsMCLAIns).MCLA || + new Promise((re) => setTimeout(re, 10)).then(waitMCLA) + ) } - await waitMCLA() - return (window as any as containsMCLAIns).MCLA + return await waitMCLA() } diff --git a/docs/.vitepress/auth/github.ts b/docs/.vitepress/auth/github.ts index 5fbc1c8b..580c03a9 100644 --- a/docs/.vitepress/auth/github.ts +++ b/docs/.vitepress/auth/github.ts @@ -58,7 +58,7 @@ async function onAuthDone(): Promise { } var state: StateI try { - state = JSON.parse(cookies.get(GH_OAUTH_STATE_NAME)) + state = JSON.parse(cookies.get(GH_OAUTH_STATE_NAME) as string) } catch (err) { console.debug("Could not parse cookie", GH_OAUTH_STATE_NAME, err) return null @@ -74,7 +74,7 @@ async function onAuthDone(): Promise { withCredentials: true, }) const data = new URLSearchParams(resp.data) - token = data.get("access_token") + token = data.get("access_token") as string } catch (err) { console.error("auth failed:", err) return null diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index a581ecdf..846c2577 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,6 +1,6 @@ -import { withPwa } from "@vite-pwa/vitepress" import process from "node:process" import { defineConfig } from "vitepress" +import { withPwa } from "@vite-pwa/vitepress" import { pwa } from "./scripts/pwa" const COMMIT_ID = process.env.CF_PAGES_COMMIT_SHA || "local" @@ -188,6 +188,6 @@ export default withPwa( ], ], - pwa, + pwa: pwa, }), ) diff --git a/docs/.vitepress/theme/components/Analyzer.vue b/docs/.vitepress/theme/components/Analyzer.vue index 8a902e92..16dae5d7 100644 --- a/docs/.vitepress/theme/components/Analyzer.vue +++ b/docs/.vitepress/theme/components/Analyzer.vue @@ -19,15 +19,19 @@ import { } from "../../analyzers/mcla" // 类型&接口定义 -interface SolutionOk { - ok: boolean - /* if ok */ - res?: Solution - /* else */ - id?: number // solution id - error?: string +interface SolutionOkSuccess { + ok: true + res: Solution } +interface SolutionOkFailed { + ok: false + id: number // solution id + error: string +} + +type SolutionOk = SolutionOkSuccess | SolutionOkFailed + interface ErrDesc { error: string message: string @@ -113,25 +117,26 @@ const router = useRouter() const utf8Decoder = new TextDecoder("utf-8") // 元素引用 -const fileUploader = ref(null) +const fileUploader = ref() // 模版变量初始化 +const mclaLoadingProcess = ref(0) const analyzerBackgroundColor = ref("") const analyzing = ref(false) const analysisShowResult = ref(false) -const analysisResults: Ref = ref(null) +const analysisResults: Ref = ref([]) const isBtnDisabled = ref(false) const labelMsg = ref("未选择文件") const btnMsg = ref("开始上传") const analysisResultMsg = ref("") -const redirectUrl = ref(null) +const redirectUrl: Ref = ref(null) const redirectMsg = ref("导航到解决方案") const showAnalyzingIcon = ref(false) watch( analyzing, (() => { // we have to wait a second before trigger the analyzing icon to make better performance - var switchInterval = null + var switchInterval: Promise | null = null return async () => { if (switchInterval) { await switchInterval @@ -147,7 +152,7 @@ watch( ) // 公共变量 -var MCLA: MCLAAPI = null +var MCLA: MCLAAPI | null = null var launcher = "Unknown" const SYSTEM_URL = "/client/system.html" // 系统问题 @@ -156,25 +161,27 @@ const MODS_URL = "/client/mods.html" // Mod 问题 const MIXIN_URL = "/mixin.html" // Mod 问题 // 阻止浏览器默认拖拽行为 -function handleDragEnter() { +function handleDragEnter(): void { analyzerBackgroundColor.value = "rgba(255,255,255,0.5)" } // 动画 -function handleDragOver() { +function handleDragOver(): void { analyzerBackgroundColor.value = "rgba(255,255,255,0.5)" } // 动画 -function handleDragLeave() { +function handleDragLeave(): void { analyzerBackgroundColor.value = "var(--vp-custom-block-tip-bg)" } // 动画 -function handleDrop(e) { - const files = e.dataTransfer.files // 获取拖拽过来的文件 - // 处理文件 - handleDropFiles(files) +function handleDrop(e: DragEvent) { + const files = e.dataTransfer?.files // 获取拖拽过来的文件 + if (files) { + // 处理文件 + handleDropFiles(files) + } } /** @@ -201,7 +208,7 @@ function clean() { function checkFiles(): Promise { const fup = fileUploader.value - return analyzeFiles(fup.files) + return analyzeFiles(fup.files as FileList) } /** @@ -235,7 +242,7 @@ const fallbackAnalysisDoneErr = new _fallbackAnalysisDoneErr() async function startAnalysis(files: File[]): Promise { if (analyzing.value) { console.error("日志已经开始分析了?") - return + return false } analyzing.value = true console.log("开始分析日志") @@ -259,7 +266,7 @@ async function startAnalysis(files: File[]): Promise { console.debug("MCLA is not loaded") } if (analysisResults.value.length === 0) { - return logAnalysis(file.text).then((ok) => { + return logAnalysis(file.text as string).then((ok) => { if (ok) { throw fallbackAnalysisDoneErr } @@ -292,7 +299,7 @@ async function startAnalysis(files: File[]): Promise { showAnalysisResult( "Success", "MCLA 分析完成, 但您不应该在页面上看到本消息 -.-", - "https://github.com/kmcsr/mcla", + "https://github.com/GlobeMC/mcla", "Multiple reasons", ) return true @@ -412,8 +419,11 @@ async function readFiles(file: MemFile, filename?: string): Promise { // MCLA错误分析 async function mclAnalysis(file: MemFile): Promise { + if (!MCLA) { + return + } const filepath = file.path - const resultIter = await MCLA.analyzeLogErrorsIter(file.text) + const resultIter = await MCLA.analyzeLogErrorsIter(file.text as string) var promises: Promise[] = [] for await (const result of resultIter) { if (!result.matched || result.matched.length === 0) { @@ -428,13 +438,17 @@ async function mclAnalysis(file: MemFile): Promise { return Promise.all( errorDesc.solutions.map((id) => axios - .get(`${MCLA_GH_DB_PREFIX}/solutions/${id}.json`) - .then((res) => ({ ok: true, res: res.data })) - .catch((err) => ({ - ok: false, - id: id, - error: String(err), - })), + .get(`${MCLA_GH_DB_PREFIX}/solutions/${id}.json`) + .then( + (res): SolutionOkSuccess => ({ ok: true, res: res.data }), + ) + .catch( + (err): SolutionOkFailed => ({ + ok: false, + id: id, + error: String(err), + }), + ), ), ).then((solutions) => ({ match: match, @@ -731,13 +745,15 @@ async function logAnalysis(log: string): Promise { ) { // 正则匹配单引号内内容 let matches = spilted[key].match(/'([^']+)'/g) - missingMod.push( - matches[0].replace(/'/g, "") + - " " + // Mod 名称,例如 'oculus' - matches[2] - .replace(/'/g, "") - .replace(/\$\{minecraft_version\}/g, "MinecraftVersion"), // Mod 版本,例如 '[1.4,)',之后可以把最低 / 最高版本提取出来解析一遍 - ) + if (matches) { + missingMod.push( + matches[0].replace(/'/g, "") + + " " + // Mod 名称,例如 'oculus' + matches[2] + .replace(/'/g, "") + .replace(/\$\{minecraft_version\}/g, "MinecraftVersion"), // Mod 版本,例如 '[1.4,)',之后可以把最低 / 最高版本提取出来解析一遍 + ) + } } } missingMod = Array.from(new Set(missingMod)) // 数组去重 @@ -909,7 +925,12 @@ async function logAnalysis(log: string): Promise { * @param {string} result_url 重定向 Url。 * @param {string} status_msg 状态信息。 */ -function showAnalysisResult(status, msg, result_url, status_msg) { +function showAnalysisResult( + status: string, + msg: string, + result_url: string, + status_msg: string, +) { console.log("展示分析结果:(" + status + ") " + msg) // 信息更改 redirectUrl.value = result_url @@ -920,13 +941,20 @@ function showAnalysisResult(status, msg, result_url, status_msg) { finishAnalysis(status, status_msg) } -function umamiTrack(...args) { - if (window.umami) { - try { - umami.track(...args) - } catch (err) { - console.error("umami error:", err) - } +// Based on https://umami.is/docs/tracker-functions +declare var umami: { + track(view_properties?: { website: string; [key: string]: string }): void + track( + event_name: string, + event_data?: { [key: string]: string | number }, + ): void +} + +function umamiTrack(...args: any[]) { + try { + umami.track(...args) + } catch (err) { + console.error("umami error:", err) } } @@ -988,7 +1016,7 @@ function finishAnalysis(status: string, msg: string) { analysisShowResult.value = true analysisResultMsg.value = "MCLA 分析器意外退出,请点击下方按钮前往 GitHub 反馈。" - redirectUrl.value = "https://github.com/kmcsr/mcla/issues/new" + redirectUrl.value = "https://github.com/GlobeMC/mcla/issues/new" redirectMsg.value = "提交反馈" umamiTrack("Analysis Error", { Status: "MCLA_Error", @@ -1007,7 +1035,7 @@ function finishAnalysis(status: string, msg: string) { } } -function redirectTo(url?: string, newTab?: boolean) { +function redirectTo(url: string | null, newTab?: boolean) { if (!url) { return } @@ -1029,7 +1057,7 @@ function redirectTo(url?: string, newTab?: boolean) { onBeforeMount(async () => { try { - MCLA = await loadMCLA() + MCLA = await loadMCLA(mclaLoadingProcess) } catch (err) { console.error("Couldn't load MCLA:", err) } @@ -1063,6 +1091,9 @@ onUnmounted(() => {

{{ labelMsg }}

+
+ (MCLA 加载中 {{ (mclaLoadingProcess * 100).toFixed(2) }}%) +
+ + Gist 链接 + diff --git a/docs/.vitepress/theme/components/TransitionExpand.vue b/docs/.vitepress/theme/components/TransitionExpand.vue index 12bba74b..26c91fc0 100644 --- a/docs/.vitepress/theme/components/TransitionExpand.vue +++ b/docs/.vitepress/theme/components/TransitionExpand.vue @@ -1,7 +1,7 @@