From 0dd7b4d802c26d98aebb9a9e3e8ae17136f5be97 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 11 Feb 2025 00:40:52 +0300 Subject: [PATCH 1/8] plugin: register custom channel later --- src/viewerConnector.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/viewerConnector.ts b/src/viewerConnector.ts index 692056e3..5096ba4d 100644 --- a/src/viewerConnector.ts +++ b/src/viewerConnector.ts @@ -126,9 +126,9 @@ export const getWsProtocolStream = async (url: string) => { const CHANNEL_NAME = 'minecraft-web-client:data' export const handleCustomChannel = async () => { - // await new Promise(resolve => { - // bot._client.once('login', resolve) - // }) + await new Promise(resolve => { + bot._client.once('login', resolve) + }) bot._client.registerChannel(CHANNEL_NAME, ['string', []], true) const toCleanup = [] as Array<() => void> From 85fbe0cec077fbff035d3f1373f40f28901d00b6 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 11 Feb 2025 01:08:41 +0300 Subject: [PATCH 2/8] seems all versions auth is supported, remove restriction --- src/devtools.ts | 27 +++++++++++++++++++++------ src/index.ts | 8 ++++---- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/devtools.ts b/src/devtools.ts index 4aa10b11..6da89f20 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -45,8 +45,12 @@ customEvents.on('gameLoaded', () => { }) window.inspectPacket = (packetName, isFromClient = false, fullOrListener: boolean | ((...args) => void) = false) => { + if (typeof isFromClient === 'function') { + fullOrListener = isFromClient + isFromClient = false + } const listener = typeof fullOrListener === 'function' - ? (name, ...args) => fullOrListener(name, ...args) + ? (name, ...args) => fullOrListener(...args, name) : (name, ...args) => { const displayName = name === packetName ? name : `${name} (${packetName})` console.log('packet', displayName, fullOrListener ? args : args[0]) @@ -57,7 +61,16 @@ window.inspectPacket = (packetName, isFromClient = false, fullOrListener: boolea ? new RegExp('^' + packetName.replaceAll('*', '.*') + '$') : null - const packetsListener = (name, data) => { + const packetNameListener = (name, data) => { + if (pattern) { + if (pattern.test(name)) { + listener(name, data) + } + } else if (name === packetName) { + listener(name, data) + } + } + const packetListener = (data, { name }) => { if (pattern) { if (pattern.test(name)) { listener(name, data) @@ -69,16 +82,18 @@ window.inspectPacket = (packetName, isFromClient = false, fullOrListener: boolea const attach = () => { if (isFromClient) { - bot?._client.prependListener('writePacket', packetsListener) + bot?._client.prependListener('writePacket', packetNameListener) } else { - bot?._client.prependListener('packet_name', packetsListener) + bot?._client.prependListener('packet_name', packetNameListener) + bot?._client.prependListener('packet', packetListener) } } const detach = () => { if (isFromClient) { - bot?._client.removeListener('writePacket', packetsListener) + bot?._client.removeListener('writePacket', packetNameListener) } else { - bot?._client.removeListener('packet_name', packetsListener) + bot?._client.removeListener('packet_name', packetNameListener) + bot?._client.removeListener('packet', packetListener) } } attach() diff --git a/src/index.ts b/src/index.ts index 199a4083..7547c453 100644 --- a/src/index.ts +++ b/src/index.ts @@ -393,10 +393,10 @@ export async function connect (connectOptions: ConnectOptions) { const downloadMcData = async (version: string) => { if (dataDownloaded) return dataDownloaded = true - if (connectOptions.authenticatedAccount && (versionToNumber(version) < versionToNumber('1.19.4') || versionToNumber(version) >= versionToNumber('1.21'))) { - // todo support it (just need to fix .export crash) - throw new UserError('Microsoft authentication is only supported on 1.19.4 - 1.20.6 (at least for now)') - } + // if (connectOptions.authenticatedAccount && (versionToNumber(version) < versionToNumber('1.19.4') || versionToNumber(version) >= versionToNumber('1.21'))) { + // // todo support it (just need to fix .export crash) + // throw new UserError('Microsoft authentication is only supported on 1.19.4 - 1.20.6 (at least for now)') + // } await downloadMcDataOnConnect(version) try { From d05898ab1cdb393e2120c51fd8d78fdc23948738 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 11 Feb 2025 15:51:56 +0300 Subject: [PATCH 3/8] display stack on app crash, more advanced logic on app refresh --- index.html | 43 +++++++++++++++++--- src/appParams.ts | 1 + src/index.ts | 3 ++ src/react/MainMenuRenderApp.tsx | 69 +++++++++++++++++++++++++-------- src/testCrasher.ts | 5 +++ 5 files changed, 100 insertions(+), 21 deletions(-) create mode 100644 src/testCrasher.ts diff --git a/index.html b/index.html index d662cbb6..8e85c039 100644 --- a/index.html +++ b/index.html @@ -25,6 +25,8 @@
Loading...
A true Minecraft client in your browser!
+ +
` @@ -35,16 +37,28 @@ document.documentElement.appendChild(loadingDivElem) } // load error handling - const onError = (message) => { - console.log(message) + const onError = (errorOrMessage, log = false) => { + const message = errorOrMessage instanceof Error ? (errorOrMessage.stack ?? errorOrMessage.message) : errorOrMessage + if (log) console.log(message) if (document.querySelector('.initial-loader') && document.querySelector('.initial-loader').querySelector('.title').textContent !== 'Error') { document.querySelector('.initial-loader').querySelector('.title').textContent = 'Error' - document.querySelector('.initial-loader').querySelector('.subtitle').textContent = message + const [errorMessage, ...errorStack] = message.split('\n') + document.querySelector('.initial-loader').querySelector('.subtitle').textContent = errorMessage + document.querySelector('.initial-loader').querySelector('.advanced-info').textContent = errorStack.join('\n') if (window.navigator.maxTouchPoints > 1) window.location.hash = '#dev' // show eruda + // unregister all sw + if (window.navigator.serviceWorker) { + window.navigator.serviceWorker.getRegistrations().then(registrations => { + registrations.forEach(registration => { + registration.unregister() + }) + }) + } } + window.lastError = errorOrMessage instanceof Error ? errorOrMessage : new Error(errorOrMessage) } - window.addEventListener('unhandledrejection', (e) => onError(e.reason)) - window.addEventListener('error', (e) => onError(e.message)) + window.addEventListener('unhandledrejection', (e) => onError(e.reason, true)) + window.addEventListener('error', (e) => onError(e.error ?? e.message)) } insertLoadingDiv() document.addEventListener('DOMContentLoaded', () => { @@ -61,6 +75,25 @@ import('https://cdn.skypack.dev/eruda').then(({ default: eruda }) => { eruda.init() }) + Promise.all([import('https://cdn.skypack.dev/stacktrace-gps'), import('https://cdn.skypack.dev/error-stack-parser')]).then(async ([{ default: StackTraceGPS }, { default: ErrorStackParser }]) => { + if (!window.lastError) return + + let stackFrames = []; + if (window.lastError instanceof Error) { + stackFrames = ErrorStackParser.parse(window.lastError); + } + console.log('stackFrames', stackFrames) + const gps = new StackTraceGPS() + const mappedFrames = await Promise.all( + stackFrames.map(frame => gps.pinpoint(frame)) + ); + console.log('mappedFrames', mappedFrames) + + const stackTrace = mappedFrames + .map(frame => `at ${frame.functionName} (${frame.fileName}:${frame.lineNumber}:${frame.columnNumber})`) + .join('\n'); + console.log('stackTrace', stackTrace) + }) } } checkLoadEruda() diff --git a/src/appParams.ts b/src/appParams.ts index 0b6bd6d8..61ee9e07 100644 --- a/src/appParams.ts +++ b/src/appParams.ts @@ -38,6 +38,7 @@ export type AppQsParams = { // Misc params suggest_save?: string noPacketsValidation?: string + testCrashApp?: string } export type AppQsParamsArray = { diff --git a/src/index.ts b/src/index.ts index 7547c453..c075e6c4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,7 @@ /* eslint-disable import/order */ import './importsWorkaround' import './styles.css' +import './testCrasher' import './globals' import './devtools' import './entities' @@ -160,6 +161,8 @@ if (isIphone) { document.documentElement.style.setProperty('--hud-bottom-max', '21px') // env-safe-aria-inset-bottom } +if (appQueryParams.testCrashApp === '2') throw new Error('test') + // Create viewer const viewer: import('renderer/viewer/lib/viewer').Viewer = new Viewer(renderer) window.viewer = viewer diff --git a/src/react/MainMenuRenderApp.tsx b/src/react/MainMenuRenderApp.tsx index c7bc99e5..c72b89e0 100644 --- a/src/react/MainMenuRenderApp.tsx +++ b/src/react/MainMenuRenderApp.tsx @@ -10,23 +10,60 @@ import { openFilePicker, copyFilesAsync, mkdirRecursive, openWorldDirectory, rem import MainMenu from './MainMenu' import { DiscordButton } from './DiscordButton' +const isMainMenu = () => { + return activeModalStack.length === 0 && !miscUiState.gameLoaded +} + const refreshApp = async (failedUpdate = false) => { - const registration = await navigator.serviceWorker.getRegistration() - await registration?.unregister() - if (failedUpdate) { - await new Promise(resolve => { - setTimeout(resolve, 2000) - }) - } - if (activeModalStack.length !== 0) return - if (failedUpdate) { - sessionStorage.justReloaded = false - // try to force bypass cache - location.search = '?update=true' - } else { - window.justReloaded = true - sessionStorage.justReloaded = true - window.location.reload() + try { + const registration = await navigator.serviceWorker.getRegistration() + if (registration) { + // First, disconnect all clients + const clients = await window.clients?.matchAll() || [] + await Promise.all(clients.map(client => client.postMessage('SKIP_WAITING'))) + + // Force the waiting service worker to become active + if (registration.waiting) { + registration.waiting.postMessage('SKIP_WAITING') + } + + // Add timeout to prevent infinite waiting + const unregisterPromise = registration.unregister() + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('SW unregister timeout')), 3000) + }) + + await Promise.race([unregisterPromise, timeoutPromise]) + .catch(err => { + console.warn('SW unregister error:', err) + if (isMainMenu()) { + alert('Failed to unregister SW: ' + err) + } + }) + } + + if (failedUpdate) { + await new Promise(resolve => { setTimeout(resolve, 2000) }) + } + + if (!isMainMenu()) return + + if (failedUpdate) { + sessionStorage.justReloaded = false + // try to force bypass cache + location.search = '?update=true' + } else { + window.justReloaded = true + sessionStorage.justReloaded = true + window.location.reload() + } + } catch (err) { + console.error('Failed to refresh app:', err) + if (!isMainMenu()) { + alert('Critical error on refreshApp: ' + err) + // Fallback to force reload if something goes wrong + window.location.reload() + } } } diff --git a/src/testCrasher.ts b/src/testCrasher.ts new file mode 100644 index 00000000..0383c2a6 --- /dev/null +++ b/src/testCrasher.ts @@ -0,0 +1,5 @@ +import { appQueryParams } from './appParams' + +if (appQueryParams.testCrashApp === '1') { + throw new Error('test error') +} From 946fc26d86612a198d17f011dcff7f5d7572539c Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 11 Feb 2025 16:26:17 +0300 Subject: [PATCH 4/8] correctly open release link, show build info --- .github/workflows/next-deploy.yml | 2 +- .github/workflows/preview.yml | 2 +- rsbuild.config.ts | 3 +++ src/react/MainMenu.tsx | 10 +++++++++- src/react/MainMenuRenderApp.tsx | 2 +- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/next-deploy.yml b/.github/workflows/next-deploy.yml index 0856285c..665abb30 100644 --- a/.github/workflows/next-deploy.yml +++ b/.github/workflows/next-deploy.yml @@ -29,7 +29,7 @@ jobs: run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} - name: Write Release Info run: | - echo "{\"latestTag\": \"$(git rev-parse --short $GITHUB_SHA)\"}" > assets/release.json + echo "{\"latestTag\": \"$(git rev-parse --short $GITHUB_SHA)\", \"isCommit\": true}" > assets/release.json - name: Build Project Artifacts run: vercel build --token=${{ secrets.VERCEL_TOKEN }} - run: pnpm build-storybook diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 7ad16c2e..18c80e8c 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -58,7 +58,7 @@ jobs: run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} - name: Write Release Info run: | - echo "{\"latestTag\": \"$(git rev-parse --short ${{ github.event.pull_request.head.sha }})\"}" > assets/release.json + echo "{\"latestTag\": \"$(git rev-parse --short ${{ github.event.pull_request.head.sha }})\", \"isCommit\": true}" > assets/release.json - name: Build Project Artifacts run: vercel build --token=${{ secrets.VERCEL_TOKEN }} - run: pnpm build-storybook diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 04edd23d..a93e2c84 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -23,11 +23,13 @@ const dev = process.env.NODE_ENV === 'development' const disableServiceWorker = process.env.DISABLE_SERVICE_WORKER === 'true' let releaseTag +let releaseLink let releaseChangelog if (fs.existsSync('./assets/release.json')) { const releaseJson = JSON.parse(fs.readFileSync('./assets/release.json', 'utf8')) releaseTag = releaseJson.latestTag + releaseLink = releaseJson.isCommit ? `/commit/${releaseJson.latestTag}` : `/releases/${releaseJson.latestTag}` releaseChangelog = releaseJson.changelog?.replace(//, '') } @@ -59,6 +61,7 @@ const appConfig = defineConfig({ JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`), 'process.env.DEPS_VERSIONS': JSON.stringify({}), 'process.env.RELEASE_TAG': JSON.stringify(releaseTag), + 'process.env.RELEASE_LINK': JSON.stringify(releaseLink), 'process.env.RELEASE_CHANGELOG': JSON.stringify(releaseChangelog), 'process.env.DISABLE_SERVICE_WORKER': JSON.stringify(disableServiceWorker), }, diff --git a/src/react/MainMenu.tsx b/src/react/MainMenu.tsx index ff0ab4bd..fc770ad1 100644 --- a/src/react/MainMenu.tsx +++ b/src/react/MainMenu.tsx @@ -58,6 +58,14 @@ export default ({ { delay: 500 } ) + const versionLongPress = useLongPress( + () => { + const buildDate = process.env.BUILD_VERSION ? new Date(process.env.BUILD_VERSION) : null + alert(`BUILD INFO:\n${buildDate?.toLocaleString() || 'Development build'}`) + }, + () => onVersionTextClick?.(), + ) + const connectToServerLongPress = useLongPress( () => { if (process.env.NODE_ENV === 'development') { @@ -147,7 +155,7 @@ export default ({
- {versionText} + {versionText} { await refreshApp() }} onVersionTextClick={async () => { - openGithub('/releases') + openGithub(process.env.RELEASE_LINK) }} versionText={process.env.RELEASE_TAG} /> From cf8d8e51fc834f654ff7be757e6a03ec97a05b18 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 11 Feb 2025 16:54:58 +0300 Subject: [PATCH 5/8] fix quickconnect port ignored regression --- src/parseServerAddress.ts | 11 +++++++---- src/react/ServersListProvider.tsx | 2 +- src/utils.test.ts | 7 ++++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/parseServerAddress.ts b/src/parseServerAddress.ts index 4068c3af..17331252 100644 --- a/src/parseServerAddress.ts +++ b/src/parseServerAddress.ts @@ -2,12 +2,12 @@ export const parseServerAddress = (address: string | undefined, removeHttp = true): ParsedServerAddress => { if (!address) { - return { host: '', isWebSocket: false } + return { host: '', isWebSocket: false, serverIpFull: '' } } const isWebSocket = address.startsWith('ws://') || address.startsWith('wss://') if (isWebSocket) { - return { host: address, isWebSocket: true } + return { host: address, isWebSocket: true, serverIpFull: address } } if (removeHttp) { @@ -33,11 +33,13 @@ export const parseServerAddress = (address: string | undefined, removeHttp = tru } } + const host = parts.join(':') return { - host: parts.join(':'), + host, ...(port ? { port } : {}), ...(version ? { version } : {}), - isWebSocket: false + isWebSocket: false, + serverIpFull: port ? `${host}:${port}` : host } } @@ -46,4 +48,5 @@ export interface ParsedServerAddress { port?: string version?: string isWebSocket: boolean + serverIpFull: string } diff --git a/src/react/ServersListProvider.tsx b/src/react/ServersListProvider.tsx index 704c77e6..e0147a33 100644 --- a/src/react/ServersListProvider.tsx +++ b/src/react/ServersListProvider.tsx @@ -245,7 +245,7 @@ const Inner = ({ hidden, customServersList }: { hidden?: boolean, customServersL } const parsed = parseServerAddress(parts.join(':')) overrides = { - ip: parsed.host, + ip: parsed.serverIpFull, versionOverride: parsed.version, authenticatedAccountOverride: msAuth ? true : undefined, // todo popup selector } diff --git a/src/utils.test.ts b/src/utils.test.ts index 14505ae2..00569bf4 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -1,5 +1,10 @@ import { describe, expect, it } from 'vitest' -import { parseServerAddress } from './parseServerAddress' +import { parseServerAddress as parseServerAddressOriginal } from './parseServerAddress' + +const parseServerAddress = (address: string | undefined, removeHttp = true) => { + const { serverIpFull, ...result } = parseServerAddressOriginal(address, removeHttp) + return result +} describe('parseServerAddress', () => { it('should handle undefined input', () => { From 8595d545a53cb71108d143e054b6f79698dd2b7a Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 11 Feb 2025 17:11:36 +0300 Subject: [PATCH 6/8] Restore support for old browsers! Restore and revisit browserslist --- package.json | 9 +++++++++ src/index.ts | 3 --- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 3084d5dd..171da2f3 100644 --- a/package.json +++ b/package.json @@ -168,6 +168,15 @@ "cypress-plugin-snapshots": "^1.4.4", "systeminformation": "^5.21.22" }, + "browserslist": [ + "iOS >= 14", + "Android >= 13", + "Chrome >= 103", + "not dead", + "not ie <= 11", + "not op_mini all", + "> 0.5%" + ], "pnpm": { "overrides": { "buffer": "^6.0.3", diff --git a/src/index.ts b/src/index.ts index c075e6c4..778cb026 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,9 +18,6 @@ import protocolMicrosoftAuth from 'minecraft-protocol/src/client/microsoftAuth' import microsoftAuthflow from './microsoftAuthflow' import { Duplex } from 'stream' -import 'core-js/features/array/at' -import 'core-js/features/promise/with-resolvers' - import './scaleInterface' import { initWithRenderer } from './topRightStats' import PrismarineBlock from 'prismarine-block' From 90e002a3a180a82cd94f91f72a131bec173394e9 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 11 Feb 2025 17:12:39 +0300 Subject: [PATCH 7/8] fix lint --- src/devtools.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/devtools.ts b/src/devtools.ts index 6da89f20..6f54eaf1 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -71,13 +71,7 @@ window.inspectPacket = (packetName, isFromClient = false, fullOrListener: boolea } } const packetListener = (data, { name }) => { - if (pattern) { - if (pattern.test(name)) { - listener(name, data) - } - } else if (name === packetName) { - listener(name, data) - } + packetNameListener(name, data) } const attach = () => { From 193616b14705825c4c3ef0405844ee0595bbff1f Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Tue, 11 Feb 2025 17:19:42 +0300 Subject: [PATCH 8/8] fix npm lib building --- src/react/MessageFormattedString.tsx | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/react/MessageFormattedString.tsx b/src/react/MessageFormattedString.tsx index 97cef89c..09797076 100644 --- a/src/react/MessageFormattedString.tsx +++ b/src/react/MessageFormattedString.tsx @@ -1,6 +1,5 @@ import { useMemo } from 'react' import { fromFormattedString } from '@xmcl/text-component' -import nbt from 'prismarine-nbt' import { ErrorBoundary } from '@zardoy/react-util' import { formatMessage } from '../chatUtils' import MessageFormatted from './MessageFormatted' @@ -13,16 +12,16 @@ export default ({ message, fallbackColor, className }: { }) => { const messageJson = useMemo(() => { if (!message) return null - const transformIfNbt = (x) => { - if (typeof x === 'object' && x?.type) return nbt.simplify(x) as Record - // if (Array.isArray(x)) return x.map(transformIfNbt) - // if (typeof x === 'object') return Object.fromEntries(Object.entries(x).map(([k, v]) => [k, transformIfNbt(v)])) - return x - } - if (typeof message === 'object' && message.text?.text?.type) { - message.text.text = transformIfNbt(message.text.text) - message.text.extra = transformIfNbt(message.text.extra) - } + // const transformIfNbt = (x) => { + // if (typeof x === 'object' && x?.type) return nbt.simplify(x) as Record + // // if (Array.isArray(x)) return x.map(transformIfNbt) + // // if (typeof x === 'object') return Object.fromEntries(Object.entries(x).map(([k, v]) => [k, transformIfNbt(v)])) + // return x + // } + // if (typeof message === 'object' && message.text?.text?.type) { + // message.text.text = transformIfNbt(message.text.text) + // message.text.extra = transformIfNbt(message.text.extra) + // } try { const texts = formatMessage(typeof message === 'string' ? fromFormattedString(message) : message) return texts.map(text => {