Merge branch 'next' into release
This commit is contained in:
commit
6329671f7f
16 changed files with 170 additions and 58 deletions
2
.github/workflows/next-deploy.yml
vendored
2
.github/workflows/next-deploy.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/preview.yml
vendored
2
.github/workflows/preview.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
43
index.html
43
index.html
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export type AppQsParams = {
|
|||
// Misc params
|
||||
suggest_save?: string
|
||||
noPacketsValidation?: string
|
||||
testCrashApp?: string
|
||||
}
|
||||
|
||||
export type AppQsParamsArray = {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
14
src/index.ts
14
src/index.ts
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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
5
src/testCrasher.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { appQueryParams } from './appParams'
|
||||
|
||||
if (appQueryParams.testCrashApp === '1') {
|
||||
throw new Error('test error')
|
||||
}
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue