Merge branch 'next' into release

This commit is contained in:
Vitaly Turovsky 2025-02-11 17:19:57 +03:00
commit 6329671f7f
16 changed files with 170 additions and 58 deletions

View file

@ -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

View file

@ -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

View file

@ -25,6 +25,8 @@
<div>
<div style="font-size: calc(var(--font-size) * 1.8);color: lightgray;" class="title">Loading...</div>
<div style="font-size: var(--font-size);color: rgb(176, 176, 176);margin-top: 3px;text-align: center" class="subtitle">A true Minecraft client in your browser!</div>
<!-- small text pre -->
<div style="font-size: calc(var(--font-size) * 0.6);color: rgb(150, 150, 150);margin-top: 3px;text-align: center;white-space: pre-line;" class="advanced-info"></div>
</div>
</div>
`
@ -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()

View file

@ -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",

View file

@ -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(/<!-- bump-type:[\w]+ -->/, '')
}
@ -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),
},

View file

@ -38,6 +38,7 @@ export type AppQsParams = {
// Misc params
suggest_save?: string
noPacketsValidation?: string
testCrashApp?: string
}
export type AppQsParamsArray = {

View file

@ -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,7 @@ 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)
@ -66,19 +70,24 @@ window.inspectPacket = (packetName, isFromClient = false, fullOrListener: boolea
listener(name, data)
}
}
const packetListener = (data, { name }) => {
packetNameListener(name, data)
}
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()

View file

@ -1,6 +1,7 @@
/* eslint-disable import/order */
import './importsWorkaround'
import './styles.css'
import './testCrasher'
import './globals'
import './devtools'
import './entities'
@ -17,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'
@ -160,6 +158,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
@ -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 {

View file

@ -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
}

View file

@ -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 ({
<div className={styles['bottom-info']}>
<div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<span style={{ fontSize: 10, color: 'gray' }} onClick={onVersionTextClick}>{versionText}</span>
<span style={{ fontSize: 10, color: 'gray' }} {...versionLongPress}>{versionText}</span>
<span
title={`${versionTitle} (click to reload)`}
onClick={onVersionStatusClick}

View file

@ -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()
}
}
}
@ -122,7 +159,7 @@ export default () => {
await refreshApp()
}}
onVersionTextClick={async () => {
openGithub('/releases')
openGithub(process.env.RELEASE_LINK)
}}
versionText={process.env.RELEASE_TAG}
/>

View file

@ -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<string, any>
// 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<string, any>
// // 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 => {

View file

@ -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
}

5
src/testCrasher.ts Normal file
View file

@ -0,0 +1,5 @@
import { appQueryParams } from './appParams'
if (appQueryParams.testCrashApp === '1') {
throw new Error('test error')
}

View file

@ -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', () => {

View file

@ -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>