Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config-json): Only either bundle or load from remote #291

Merged
merged 8 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/next-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ jobs:
echo "{\"latestTag\": \"$(git rev-parse --short $GITHUB_SHA)\", \"isCommit\": true}" > assets/release.json
- name: Build Project Artifacts
run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
env:
CONFIG_JSON_SOURCE: BUNDLED
- run: pnpm build-storybook
- name: Copy playground files
run: |
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ jobs:
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 }}
env:
CONFIG_JSON_SOURCE: BUNDLED
- run: pnpm build-storybook
- name: Copy playground files
run: |
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: vercel build --token=${{ secrets.VERCEL_TOKEN }} --prod
env:
CONFIG_JSON_SOURCE: BUNDLED
- run: pnpm build-storybook
- name: Copy playground files
run: |
Expand Down
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ RUN npm i -g [email protected]
# Build arguments
ARG DOWNLOAD_SOUNDS=false
ARG DISABLE_SERVICE_WORKER=false
ARG CONFIG_JSON_SOURCE=REMOTE
# TODO need flat --no-root-optional
RUN node ./scripts/dockerPrepare.mjs
RUN pnpm i
Expand All @@ -22,8 +23,8 @@ RUN if [ "$DOWNLOAD_SOUNDS" = "true" ] ; then node scripts/downloadSoundsMap.mjs
# ENTRYPOINT ["pnpm", "run", "run-all"]

# only for prod
RUN GITHUB_REPOSITORY=zardoy/minecraft-web-client \
DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \
RUN DISABLE_SERVICE_WORKER=$DISABLE_SERVICE_WORKER \
CONFIG_JSON_SOURCE=$CONFIG_JSON_SOURCE \
pnpm run build

# ---- Run Stage ----
Expand Down
12 changes: 9 additions & 3 deletions rsbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ const disableServiceWorker = process.env.DISABLE_SERVICE_WORKER === 'true'
let releaseTag
let releaseLink
let releaseChangelog
let githubRepositoryFallback

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]+ -->/, '')
githubRepositoryFallback = releaseJson.repository
}

const configJson = JSON.parse(fs.readFileSync('./config.json', 'utf8'))
Expand All @@ -41,6 +43,8 @@ if (dev) {
configJson.defaultProxy = ':8080'
}

const configSource = process.env.CONFIG_JSON_SOURCE || 'REMOTE'

// base options are in ./renderer/rsbuildSharedConfig.ts
const appConfig = defineConfig({
html: {
Expand All @@ -66,13 +70,13 @@ const appConfig = defineConfig({
'process.env.BUILD_VERSION': JSON.stringify(!dev ? buildingVersion : 'undefined'),
'process.env.MAIN_MENU_LINKS': JSON.stringify(process.env.MAIN_MENU_LINKS),
'process.env.GITHUB_URL':
JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}`}`),
JSON.stringify(`https://github.com/${process.env.GITHUB_REPOSITORY || `${process.env.VERCEL_GIT_REPO_OWNER}/${process.env.VERCEL_GIT_REPO_SLUG}` || githubRepositoryFallback}`),
'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),
'process.env.INLINED_APP_CONFIG': JSON.stringify(configJson),
'process.env.INLINED_APP_CONFIG': JSON.stringify(configSource === 'BUNDLED' ? configJson : null),
},
},
server: {
Expand Down Expand Up @@ -109,7 +113,9 @@ const appConfig = defineConfig({
fs.copyFileSync('./assets/release.json', './dist/release.json')
}

fs.writeFileSync('./dist/config.json', JSON.stringify(configJson), 'utf8')
if (configSource === 'REMOTE') {
fs.writeFileSync('./dist/config.json', JSON.stringify(configJson), 'utf8')
}
if (fs.existsSync('./generated/sounds.js')) {
fs.copyFileSync('./generated/sounds.js', './dist/sounds.js')
}
Expand Down
22 changes: 20 additions & 2 deletions scripts/dockerPrepare.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,27 @@ import path from 'path'
import { fileURLToPath } from 'url'
import { execSync } from 'child_process'

// write release tag
// Get repository from git config
const getGitRepository = () => {
try {
const gitConfig = fs.readFileSync('.git/config', 'utf8')
const originUrlMatch = gitConfig.match(/\[remote "origin"\][\s\S]*?url = .*?github\.com[:/](.*?)(\.git)?\n/m)
if (originUrlMatch) {
return originUrlMatch[1]
}
} catch (err) {
console.warn('Failed to read git repository from config:', err)
}
return null
}
Comment on lines +7 to +19
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use native git commands instead of parsing config file.

The function attempts to extract the repository information by parsing the git config file directly with regex, which is fragile and may break with different git configurations or formats.

I recommend using git commands directly through execSync, which would be more robust:

// Get repository from git config
const getGitRepository = () => {
    try {
-        const gitConfig = fs.readFileSync('.git/config', 'utf8')
-        const originUrlMatch = gitConfig.match(/\[remote "origin"\][\s\S]*?url = .*?github\.com[:/](.*?)(\.git)?\n/m)
-        if (originUrlMatch) {
-            return originUrlMatch[1]
-        }
+        const remoteUrl = execSync('git config --get remote.origin.url').toString().trim()
+        // Extract repo from GitHub URL format (handles both HTTPS and SSH formats)
+        const githubRepoMatch = remoteUrl.match(/github\.com[:/](.*?)(\.git)?$/)
+        if (githubRepoMatch) {
+            return githubRepoMatch[1]
+        }
+        console.warn('Remote URL does not appear to be a GitHub repository:', remoteUrl)
    } catch (err) {
        console.warn('Failed to read git repository from config:', err)
    }
    return null
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Get repository from git config
const getGitRepository = () => {
try {
const gitConfig = fs.readFileSync('.git/config', 'utf8')
const originUrlMatch = gitConfig.match(/\[remote "origin"\][\s\S]*?url = .*?github\.com[:/](.*?)(\.git)?\n/m)
if (originUrlMatch) {
return originUrlMatch[1]
}
} catch (err) {
console.warn('Failed to read git repository from config:', err)
}
return null
}
// Get repository from git config
const getGitRepository = () => {
try {
const remoteUrl = execSync('git config --get remote.origin.url').toString().trim()
// Extract repo from GitHub URL format (handles both HTTPS and SSH formats)
const githubRepoMatch = remoteUrl.match(/github\.com[:/](.*?)(\.git)?$/)
if (githubRepoMatch) {
return githubRepoMatch[1]
}
console.warn('Remote URL does not appear to be a GitHub repository:', remoteUrl)
} catch (err) {
console.warn('Failed to read git repository from config:', err)
}
return null
}


// write release tag and repository info
const commitShort = execSync('git rev-parse --short HEAD').toString().trim()
fs.writeFileSync('./assets/release.json', JSON.stringify({ latestTag: `${commitShort} (docker)` }), 'utf8')
const repository = getGitRepository()
fs.writeFileSync('./assets/release.json', JSON.stringify({
latestTag: `${commitShort} (docker)`,
repository
}), 'utf8')

const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
delete packageJson.optionalDependencies
Expand Down
59 changes: 59 additions & 0 deletions src/appConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { disabledSettings, options, qsOptions } from './optionsStorage'
import { miscUiState } from './globalState'
import { setLoadingScreenStatus } from './appStatus'

export type AppConfig = {
// defaultHost?: string
// defaultHostSave?: string
defaultProxy?: string
// defaultProxySave?: string
// defaultVersion?: string
peerJsServer?: string
peerJsServerFallback?: string
promoteServers?: Array<{ ip, description, version? }>
mapsProvider?: string

appParams?: Record<string, any> // query string params

defaultSettings?: Record<string, any>
forceSettings?: Record<string, boolean>
// hideSettings?: Record<string, boolean>
allowAutoConnect?: boolean
pauseLinks?: Array<Array<Record<string, any>>>
}

export const loadAppConfig = (appConfig: AppConfig) => {
if (miscUiState.appConfig) {
Object.assign(miscUiState.appConfig, appConfig)
} else {
miscUiState.appConfig = appConfig
}

if (appConfig.forceSettings) {
for (const [key, value] of Object.entries(appConfig.forceSettings)) {
if (value) {
disabledSettings.value.add(key)
// since the setting is forced, we need to set it to that value
if (appConfig.defaultSettings?.[key] && !qsOptions[key]) {
options[key] = appConfig.defaultSettings[key]
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will break setting from qs params , eed to fix

}
} else {
disabledSettings.value.delete(key)
}
}
}
}

export const isBundledConfigUsed = !!process.env.INLINED_APP_CONFIG

if (isBundledConfigUsed) {
loadAppConfig(process.env.INLINED_APP_CONFIG as AppConfig ?? {})
} else {
void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably use await not void to ensure the config was loaded before the client continues to load.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I was avoiding any usage of top level awaits because I think it can break the app in some unexpected way and don't really see any benefit of using it

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I don't really see what is stopping the client from using the bundled config.json values until the remote one is loaded and imo await is the proper way to force async queries to block the app until it's loaded? If it really breaks something then that would be a bug elsewhere (and if it fails to load the file then it should of course break as the app doesn't work without the config)

// console.warn('Failed to load optional app config.json', error)
// return {}
setLoadingScreenStatus('Failed to load app config.json', true)
}).then((config: AppConfig) => {
loadAppConfig(config)
})
}
2 changes: 1 addition & 1 deletion src/appParams.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AppConfig } from './globalState'
import type { AppConfig } from './appConfig'

const qsParams = new URLSearchParams(window.location?.search ?? '')

Expand Down
4 changes: 2 additions & 2 deletions src/browserfs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ browserfs.configure({
throw e2
}
showNotification('Failed to access device storage', `Check you have free space. ${e.message}`, true)
miscUiState.appLoaded = true
miscUiState.fsReady = true
miscUiState.singleplayerAvailable = false
})
return
}
await updateTexturePackInstalledState()
miscUiState.appLoaded = true
miscUiState.fsReady = true
miscUiState.singleplayerAvailable = true
})

Expand Down
6 changes: 3 additions & 3 deletions src/customChannels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,15 @@ const registeredJeiChannel = () => {
[
{
name: 'id',
type: 'pstring',
type: ['pstring', { countType: 'i16' }]
},
{
name: 'categoryTitle',
type: 'pstring',
type: ['pstring', { countType: 'i16' }]
},
{
name: 'items',
type: 'pstring',
type: ['pstring', { countType: 'i16' }]
},
]
]
Expand Down
41 changes: 2 additions & 39 deletions src/globalState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { WorldWarp } from 'flying-squid/dist/lib/modules/warps'
import type { OptionsGroupType } from './optionsGuiScheme'
import { appQueryParams } from './appParams'
import { options, disabledSettings } from './optionsStorage'
import { AppConfig } from './appConfig'

// todo: refactor structure with support of hideNext=false

Expand Down Expand Up @@ -110,26 +111,6 @@ export const showContextmenu = (items: ContextMenuItem[], { clientX, clientY })

// ---

export type AppConfig = {
// defaultHost?: string
// defaultHostSave?: string
defaultProxy?: string
// defaultProxySave?: string
// defaultVersion?: string
peerJsServer?: string
peerJsServerFallback?: string
promoteServers?: Array<{ ip, description, version? }>
mapsProvider?: string

appParams?: Record<string, any> // query string params

defaultSettings?: Record<string, any>
forceSettings?: Record<string, boolean>
// hideSettings?: Record<string, boolean>
allowAutoConnect?: boolean
pauseLinks?: Array<Array<Record<string, any>>>
}

export const miscUiState = proxy({
currentDisplayQr: null as string | null,
currentTouch: null as boolean | null,
Expand All @@ -144,32 +125,14 @@ export const miscUiState = proxy({
loadedServerIndex: '',
/** currently trying to load or loaded mc version, after all data is loaded */
loadedDataVersion: null as string | null,
appLoaded: false,
fsReady: false,
singleplayerAvailable: false,
usingGamepadInput: false,
appConfig: null as AppConfig | null,
displaySearchInput: false,
displayFullmap: false
})

export const loadAppConfig = (appConfig: AppConfig) => {
if (miscUiState.appConfig) {
Object.assign(miscUiState.appConfig, appConfig)
} else {
miscUiState.appConfig = appConfig
}

if (appConfig.forceSettings) {
for (const [key, value] of Object.entries(appConfig.forceSettings)) {
if (value) {
disabledSettings.value.delete(key)
} else {
disabledSettings.value.add(key)
}
}
}
}

export const isGameActive = (foregroundCheck: boolean) => {
if (foregroundCheck && activeModalStack.length) return false
return miscUiState.gameLoaded
Expand Down
17 changes: 4 additions & 13 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import './mineflayer/cameraShake'
import './shims/patchShims'
import './mineflayer/java-tester/index'
import './external'
import './appConfig'
import { getServerInfo } from './mineflayer/mc-protocol'
import { onGameLoad, renderSlot } from './inventoryWindows'
import { GeneralInputItem, RenderItem } from './mineflayer/items'
Expand Down Expand Up @@ -48,7 +49,6 @@ import initializePacketsReplay from './packetsReplay/packetsReplayLegacy'

import { initVR } from './vr'
import {
AppConfig,
activeModalStack,
activeModalStacks,
hideModal,
Expand All @@ -57,7 +57,6 @@ import {
miscUiState,
showModal,
gameAdditionalState,
loadAppConfig
} from './globalState'

import { parseServerAddress } from './parseServerAddress'
Expand Down Expand Up @@ -903,8 +902,9 @@ export async function connect (connectOptions: ConnectOptions) {
const reconnectOptions = sessionStorage.getItem('reconnectOptions') ? JSON.parse(sessionStorage.getItem('reconnectOptions')!) : undefined

listenGlobalEvents()
watchValue(miscUiState, async s => {
if (s.appLoaded) { // fs ready
const unsubscribe = watchValue(miscUiState, async s => {
if (s.fsReady && s.appConfig) {
unsubscribe()
if (reconnectOptions) {
sessionStorage.removeItem('reconnectOptions')
if (Date.now() - reconnectOptions.timestamp < 1000 * 60 * 2) {
Expand Down Expand Up @@ -965,15 +965,6 @@ document.body.addEventListener('touchstart', (e) => {
}, { passive: false })
// #endregion

loadAppConfig(process.env.INLINED_APP_CONFIG as AppConfig ?? {})
// load maybe updated config on the server with updated params (just in case)
void window.fetch('config.json').then(async res => res.json()).then(c => c, (error) => {
console.warn('Failed to load optional app config.json', error)
return {}
}).then((config: AppConfig | {}) => {
loadAppConfig(config)
})

// qs open actions
if (!reconnectOptions) {
downloadAndOpenFile().then((downloadAction) => {
Expand Down
19 changes: 14 additions & 5 deletions src/optionsStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { proxy, subscribe } from 'valtio/vanilla'
import { subscribeKey } from 'valtio/utils'
import { omitObj } from '@zardoy/utils'
import { appQueryParamsArray } from './appParams'
import type { AppConfig } from './globalState'
import type { AppConfig } from './appConfig'

const isDev = process.env.NODE_ENV === 'development'
const initialAppConfig = process.env.INLINED_APP_CONFIG as AppConfig ?? {}
Expand Down Expand Up @@ -188,7 +188,7 @@ subscribe(options, () => {
localStorage.options = JSON.stringify(saveOptions)
})

type WatchValue = <T extends Record<string, any>>(proxy: T, callback: (p: T, isChanged: boolean) => void) => void
type WatchValue = <T extends Record<string, any>>(proxy: T, callback: (p: T, isChanged: boolean) => void) => () => void

export const watchValue: WatchValue = (proxy, callback) => {
const watchedProps = new Set<string>()
Expand All @@ -198,10 +198,19 @@ export const watchValue: WatchValue = (proxy, callback) => {
return Reflect.get(target, p, receiver)
},
}), false)
const unsubscribes = [] as Array<() => void>
for (const prop of watchedProps) {
subscribeKey(proxy, prop, () => {
callback(proxy, true)
})
unsubscribes.push(
subscribeKey(proxy, prop, () => {
callback(proxy, true)
})
)
}

return () => {
for (const unsubscribe of unsubscribes) {
unsubscribe()
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/panorama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const updateResourcePackSupportPanorama = async () => {
}

watchValue(miscUiState, m => {
if (m.appLoaded) {
if (m.fsReady) {
// Also adds panorama on app load here
watchValue(resourcePackState, async (s) => {
const oldState = panoramaUsesResourcePack
Expand Down
Loading
Loading