mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
dev: serve game assets via sirv plugin and pre-init configuration
Restoring `yarn start` from "takes forever" back to seconds.
A previous session had symlinked `public/nitro-assets` and `public/swf`
to a sibling `Nitro-Files/` tree (~177k files) so Vite could serve them
through `publicDir`. The cost was massive: chokidar tried to install a
watcher on every file at startup and the dev server hung for minutes
on Windows. Upstream `duckietm/Nitro-V3` never does this — assets live
on a separate HTTP server referenced by URL in the JSON configs.
Changes:
- Remove the two symlinks under `public/` and add a .gitignore entry
with a note explaining why they must not come back.
- Add a small Vite plugin (`nitroAssetsServer`) that mounts `sirv` on
`/nitro-assets/*` and `/swf/*`, reading from
`../Nitro-Files/{nitro-assets,swf}`. sirv is a connect-style
middleware that bypasses chokidar entirely, so 177k files no longer
cost anything at startup. The plugin also wires the same handler
into `configurePreviewServer` so `yarn preview` keeps working.
- Drop the matching `/nitro-assets` and `/swf` entries from
`server.proxy` — they had been pointed at the auth proxy on :2096
which does not expose those paths.
- Disable `login.turnstile.enabled` in `renderer-config.json`. The
configured sitekey is Cloudflare's "always-passes" test key but the
widget still requires user interaction and blocks the login flow
in local dev.
Login flow fixes that fell out of debugging:
- `prepare()` in App.tsx ran twice under React Strict Mode (mount →
cleanup → mount). The first pass set `setShowLogin(true)`, the
second raced ahead and fell through to `onSessionExpired()`,
clobbering the login UI. Guard the effect with
`lastPrepareTriggerRef` so duplicate runs at the same trigger value
are skipped while intentional re-runs (after a successful login,
which bumps `prepareTrigger`) still go through.
- Call `GetConfiguration().init()` from `bootstrap.ts` before
importing `./index`. The renderer's ConfigurationManager logs
"Missing configuration key" the first time any key is read against
an uninitialised store, and components mounted in the first paint
(login screen, hooks, the renderer warmup) were all hitting that
path before prepare()'s deferred init landed. Pre-loading the
config means the store is already populated when React mounts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,3 +35,9 @@ Thumbs.db
|
|||||||
/public/configuration/client-mode.json
|
/public/configuration/client-mode.json
|
||||||
/public/configuration/adsense.json
|
/public/configuration/adsense.json
|
||||||
/public/configuration/hotlooks.json
|
/public/configuration/hotlooks.json
|
||||||
|
|
||||||
|
# Game assets are served by an external server (emulator/CMS), not by Vite.
|
||||||
|
# Never recreate these as symlinks inside public/ — chokidar follows them and
|
||||||
|
# the dev server takes minutes to start with 100k+ files under public/.
|
||||||
|
/public/nitro-assets
|
||||||
|
/public/swf
|
||||||
|
|||||||
+12
-1
@@ -1 +1,12 @@
|
|||||||
<div id="root"></div><script type="module" src="/src/bootstrap.ts"></script>
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||||
|
<title>Nitro</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/bootstrap.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@
|
|||||||
"postcss": "^8.5.12",
|
"postcss": "^8.5.12",
|
||||||
"postcss-nested": "^7.0.2",
|
"postcss-nested": "^7.0.2",
|
||||||
"sass": "^1.99.0",
|
"sass": "^1.99.0",
|
||||||
|
"sirv": "^3.0.2",
|
||||||
"tailwindcss": "^4.2.4",
|
"tailwindcss": "^4.2.4",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"typescript-eslint": "^8.59.1",
|
"typescript-eslint": "^8.59.1",
|
||||||
|
|||||||
+156
-18
@@ -76,18 +76,63 @@ export const App: FC<{}> = props =>
|
|||||||
const rendererPromiseRef = useRef<Promise<any>>(null);
|
const rendererPromiseRef = useRef<Promise<any>>(null);
|
||||||
const gameInitPromiseRef = useRef<Promise<void> | null>(null);
|
const gameInitPromiseRef = useRef<Promise<void> | null>(null);
|
||||||
const bootstrapDoneRef = useRef(false);
|
const bootstrapDoneRef = useRef(false);
|
||||||
|
const lastPrepareTriggerRef = useRef<number | null>(null);
|
||||||
const tickersStartedRef = useRef(false);
|
const tickersStartedRef = useRef(false);
|
||||||
const heartbeatIntervalRef = useRef<number>(null);
|
const heartbeatIntervalRef = useRef<number>(null);
|
||||||
const rememberRotateIntervalRef = useRef<number>(null);
|
const rememberRotateIntervalRef = useRef<number>(null);
|
||||||
|
const isReadyRef = useRef(false);
|
||||||
|
const reconnectInProgressRef = useRef(false);
|
||||||
|
|
||||||
|
const clearStoredCredentials = useCallback(() =>
|
||||||
|
{
|
||||||
|
ClearRememberLogin();
|
||||||
|
try { delete (window as any).NitroConfig?.['sso.ticket']; } catch {}
|
||||||
|
try { GetConfiguration().setValue('sso.ticket', ''); } catch {}
|
||||||
|
// Drop `?sso=` from the URL too — otherwise the next reload re-applies
|
||||||
|
// the same already-consumed ticket via bootstrap.ts and we loop right
|
||||||
|
// back into "Session expired" without ever showing the login form.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
|
||||||
|
if(url.searchParams.has('sso'))
|
||||||
|
{
|
||||||
|
url.searchParams.delete('sso');
|
||||||
|
window.history.replaceState({}, '', url.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fallbackToLogin = useCallback(() =>
|
||||||
|
{
|
||||||
|
// Using console.warn (not NitroLogger.log) on purpose: NitroLogger
|
||||||
|
// is gated on LOG_DEBUG, which only flips to true once startWarmup's
|
||||||
|
// GetConfiguration().init() completes. Auth-failure paths fire before
|
||||||
|
// that, so NitroLogger swallows their messages silently.
|
||||||
|
console.warn('[App] fallbackToLogin — surfacing login form, credentials cleared');
|
||||||
|
// Wipe whatever credential the server just rejected so the form is
|
||||||
|
// pristine and the next attempt isn't sabotaged by the same stale data.
|
||||||
|
clearStoredCredentials();
|
||||||
|
setHomeUrl('');
|
||||||
|
setErrorMessage('');
|
||||||
|
setIsReady(false);
|
||||||
|
setShowLogin(true);
|
||||||
|
setIsEnteringHotel(false);
|
||||||
|
}, [ clearStoredCredentials ]);
|
||||||
|
|
||||||
const showSessionExpired = useCallback(() =>
|
const showSessionExpired = useCallback(() =>
|
||||||
{
|
{
|
||||||
|
console.warn('[App] showSessionExpired — diagnostic shown (mid-game close)');
|
||||||
|
clearStoredCredentials();
|
||||||
|
|
||||||
const baseUrl = window.location.origin + '/';
|
const baseUrl = window.location.origin + '/';
|
||||||
setHomeUrl(baseUrl);
|
setHomeUrl(baseUrl);
|
||||||
setErrorMessage('Your session has expired.\nPlease log in again to enter the hotel.');
|
setErrorMessage('Your session has expired.\nPlease log in again to enter the hotel.');
|
||||||
setIsReady(false);
|
setIsReady(false);
|
||||||
setShowLogin(false);
|
setShowLogin(false);
|
||||||
setIsEnteringHotel(false);
|
setIsEnteringHotel(false);
|
||||||
}, []);
|
}, [ clearStoredCredentials ]);
|
||||||
|
|
||||||
const applySsoTicket = useCallback((ssoTicket: string) =>
|
const applySsoTicket = useCallback((ssoTicket: string) =>
|
||||||
{
|
{
|
||||||
@@ -109,10 +154,20 @@ export const App: FC<{}> = props =>
|
|||||||
{
|
{
|
||||||
const remembered = GetRememberLogin();
|
const remembered = GetRememberLogin();
|
||||||
|
|
||||||
if(!remembered) return '';
|
console.warn('[App] tryRememberLogin start', {
|
||||||
if(!remembered.token?.length && remembered.ssoTicket?.length) return remembered.ssoTicket;
|
hasRemembered: !!remembered,
|
||||||
|
hasToken: !!remembered?.token?.length,
|
||||||
|
hasStoredSso: !!remembered?.ssoTicket?.length
|
||||||
|
});
|
||||||
|
|
||||||
let allowSsoFallback = true;
|
if(!remembered?.token?.length)
|
||||||
|
{
|
||||||
|
// No remember token means we'd be reusing a one-shot ssoTicket that
|
||||||
|
// the server already consumed. Force the login screen instead.
|
||||||
|
if(remembered) ClearRememberLogin();
|
||||||
|
console.warn('[App] tryRememberLogin → no token, returning empty');
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -139,24 +194,28 @@ export const App: FC<{}> = props =>
|
|||||||
|
|
||||||
const ssoTicket = typeof payload.ssoTicket === 'string' ? payload.ssoTicket : (typeof payload.sso === 'string' ? payload.sso : '');
|
const ssoTicket = typeof payload.ssoTicket === 'string' ? payload.ssoTicket : (typeof payload.sso === 'string' ? payload.sso : '');
|
||||||
|
|
||||||
|
console.warn('[App] tryRememberLogin → remember endpoint replied', {
|
||||||
|
status: response.status,
|
||||||
|
ok: response.ok,
|
||||||
|
gotSsoTicket: !!ssoTicket
|
||||||
|
});
|
||||||
|
|
||||||
if(response.ok && ssoTicket)
|
if(response.ok && ssoTicket)
|
||||||
{
|
{
|
||||||
StoreRememberLoginFromPayload(payload, typeof payload.username === 'string' ? payload.username : remembered.username, ssoTicket);
|
StoreRememberLoginFromPayload(payload, typeof payload.username === 'string' ? payload.username : remembered.username, ssoTicket);
|
||||||
return ssoTicket;
|
return ssoTicket;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(response.status === 400 || response.status === 401 || response.status === 403)
|
|
||||||
{
|
|
||||||
allowSsoFallback = false;
|
|
||||||
ClearRememberLogin();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch(error)
|
catch(error)
|
||||||
{
|
{
|
||||||
NitroLogger.error('[LoginScreen] Remember login failed', error);
|
console.warn('[App] tryRememberLogin → fetch threw', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(allowSsoFallback && remembered.ssoTicket?.length) return remembered.ssoTicket;
|
// Any failure (rejected token, bad payload, network error) — drop the
|
||||||
|
// stored credentials. Never fall back to the cached ssoTicket: it's
|
||||||
|
// one-shot and reusing it leads straight to "Session expired".
|
||||||
|
ClearRememberLogin();
|
||||||
|
console.warn('[App] tryRememberLogin → cleared remember, returning empty');
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
}, []);
|
}, []);
|
||||||
@@ -204,8 +263,51 @@ export const App: FC<{}> = props =>
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Listen for socket closed events (code 1000 "Bye" - server rejected SSO)
|
// Mirror isReady into a ref so the socket handlers below can read the
|
||||||
useNitroEvent(NitroEventType.SOCKET_CLOSED, showSessionExpired);
|
// freshest value without needing to re-subscribe on every state change.
|
||||||
|
useEffect(() => { isReadyRef.current = isReady; }, [ isReady ]);
|
||||||
|
|
||||||
|
// Track whether a reconnect cycle is active. The renderer dispatches
|
||||||
|
// SOCKET_RECONNECTING when it starts retrying after an abnormal close
|
||||||
|
// (code != 1000/1001). On exhausted retries it fires SOCKET_RECONNECT_FAILED
|
||||||
|
// *and* a final SOCKET_CLOSED — we keep the flag set through that pair
|
||||||
|
// so ReconnectView's own overlay owns the UX and we don't double-render.
|
||||||
|
useNitroEvent(NitroEventType.SOCKET_RECONNECTING, () => { reconnectInProgressRef.current = true; });
|
||||||
|
useNitroEvent(NitroEventType.SOCKET_REAUTHENTICATED, () => { reconnectInProgressRef.current = false; });
|
||||||
|
|
||||||
|
useNitroEvent(NitroEventType.SOCKET_CLOSED, () =>
|
||||||
|
{
|
||||||
|
// Three distinct close scenarios converge here:
|
||||||
|
//
|
||||||
|
// 1. !isReady — initial handshake just failed (server replied
|
||||||
|
// with "Bye" / code 1000 to a bad SSO ticket). The user never
|
||||||
|
// had a session. Surface the login form instead of the
|
||||||
|
// misleading "Session expired" diagnostic.
|
||||||
|
//
|
||||||
|
// 2. isReady && reconnect in progress — ReconnectView already
|
||||||
|
// owns the UX (its overlay shows attempts and the "Session
|
||||||
|
// expired" message on RECONNECT_FAILED). Stay out of its way.
|
||||||
|
//
|
||||||
|
// 3. isReady && no reconnect — instant server kick mid-game
|
||||||
|
// (code 1000 from the server side). No reconnect path will
|
||||||
|
// run. Show the legacy session-expired diagnostic so the
|
||||||
|
// user knows to reload.
|
||||||
|
console.warn('[App] SOCKET_CLOSED fired', {
|
||||||
|
isReady: isReadyRef.current,
|
||||||
|
reconnectInProgress: reconnectInProgressRef.current
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!isReadyRef.current)
|
||||||
|
{
|
||||||
|
console.warn('[App] Socket closed before authentication completed — falling back to login');
|
||||||
|
fallbackToLogin();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(reconnectInProgressRef.current) return;
|
||||||
|
|
||||||
|
showSessionExpired();
|
||||||
|
});
|
||||||
|
|
||||||
useMessageEvent<LoadGameUrlEvent>(LoadGameUrlEvent, event =>
|
useMessageEvent<LoadGameUrlEvent>(LoadGameUrlEvent, event =>
|
||||||
{
|
{
|
||||||
@@ -317,11 +419,19 @@ export const App: FC<{}> = props =>
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSessionExpired = useEffectEvent(() => showSessionExpired());
|
const onSessionExpired = useEffectEvent(() => showSessionExpired());
|
||||||
|
const onInitFailure = useEffectEvent(() => fallbackToLogin());
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
{
|
{
|
||||||
const prepare = async (width: number, height: number) =>
|
const prepare = async (width: number, height: number) =>
|
||||||
{
|
{
|
||||||
|
console.warn('[App] prepare() start', {
|
||||||
|
hasNitroConfig: !!window.NitroConfig,
|
||||||
|
ssoTicketInConfig: !!window.NitroConfig?.['sso.ticket'],
|
||||||
|
hasRememberLocal: !!GetRememberLogin(),
|
||||||
|
urlSso: new URLSearchParams(window.location.search).get('sso')
|
||||||
|
});
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if(!window.NitroConfig) throw new Error('NitroConfig is not defined!');
|
if(!window.NitroConfig) throw new Error('NitroConfig is not defined!');
|
||||||
@@ -346,6 +456,13 @@ export const App: FC<{}> = props =>
|
|||||||
const rawLoginEnabled = GetConfiguration().getValue<unknown>('login.screen.enabled', false);
|
const rawLoginEnabled = GetConfiguration().getValue<unknown>('login.screen.enabled', false);
|
||||||
const loginScreenEnabled = rawLoginEnabled === true || rawLoginEnabled === 'true' || rawLoginEnabled === 1;
|
const loginScreenEnabled = rawLoginEnabled === true || rawLoginEnabled === 'true' || rawLoginEnabled === 1;
|
||||||
|
|
||||||
|
console.warn('[App] no SSO path — login gate', {
|
||||||
|
configInitError: configInitError ? String((configInitError as Error)?.message ?? configInitError) : null,
|
||||||
|
rawLoginEnabled,
|
||||||
|
rawLoginEnabledType: typeof rawLoginEnabled,
|
||||||
|
loginScreenEnabled
|
||||||
|
});
|
||||||
|
|
||||||
if(configInitError)
|
if(configInitError)
|
||||||
{
|
{
|
||||||
NitroLogger.error('[LoginScreen] Failed to load renderer-config.json — cannot resolve login.screen.enabled', configInitError);
|
NitroLogger.error('[LoginScreen] Failed to load renderer-config.json — cannot resolve login.screen.enabled', configInitError);
|
||||||
@@ -432,12 +549,27 @@ export const App: FC<{}> = props =>
|
|||||||
}
|
}
|
||||||
catch(err)
|
catch(err)
|
||||||
{
|
{
|
||||||
NitroLogger.error(err);
|
NitroLogger.error('[App] Initialization failed — falling back to login', err);
|
||||||
setIsEnteringHotel(false);
|
// Anything thrown out of the post-auth chain (renderer init,
|
||||||
onSessionExpired();
|
// asset download, GetCommunication().init()) is an init/connect
|
||||||
|
// failure, not session expiration. The credential we used is
|
||||||
|
// suspect — drop it and present the login form so the user
|
||||||
|
// can retry instead of getting stuck on a stale "Session expired".
|
||||||
|
onInitFailure();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// React Strict Mode in dev runs every effect twice (mount → cleanup → mount).
|
||||||
|
// `prepare()` is full of one-shot side effects (renderer init, websocket
|
||||||
|
// connect, NitroConfig mutation) — calling it twice with the same trigger
|
||||||
|
// value causes the second pass to race with the first and clobber state
|
||||||
|
// (e.g. the second pass falls through to onSessionExpired while the first
|
||||||
|
// had just set showLogin=true). Guard by trigger value: skip duplicate
|
||||||
|
// runs at the same trigger, but still re-run when handleAuthenticated
|
||||||
|
// bumps prepareTrigger after a successful login.
|
||||||
|
if(lastPrepareTriggerRef.current === prepareTrigger) return;
|
||||||
|
lastPrepareTriggerRef.current = prepareTrigger;
|
||||||
|
|
||||||
const { width, height } = getViewportDimensions();
|
const { width, height } = getViewportDimensions();
|
||||||
|
|
||||||
prepare(width, height);
|
prepare(width, height);
|
||||||
@@ -455,7 +587,13 @@ export const App: FC<{}> = props =>
|
|||||||
<LoadingView isError={ errorMessage.length > 0 } message={ errorMessage } homeUrl={ homeUrl } /> }
|
<LoadingView isError={ errorMessage.length > 0 } message={ errorMessage } homeUrl={ homeUrl } /> }
|
||||||
{ !isReady && showLogin && <LoginView onAuthenticated={ handleAuthenticated } isEntering={ isEnteringHotel } /> }
|
{ !isReady && showLogin && <LoginView onAuthenticated={ handleAuthenticated } isEntering={ isEnteringHotel } /> }
|
||||||
{ isReady && <MainView /> }
|
{ isReady && <MainView /> }
|
||||||
<ReconnectView />
|
{ /* Reconnect overlay must NOT render before we've actually entered
|
||||||
|
the hotel — otherwise the renderer's auto-retry on an initial
|
||||||
|
handshake failure (e.g. emulator unreachable) would cover the
|
||||||
|
login form with "Reconnecting..." → "Session expired" and the
|
||||||
|
user wouldn't be able to interact with the form we just put up
|
||||||
|
via fallbackToLogin. */ }
|
||||||
|
{ isReady && <ReconnectView /> }
|
||||||
<Base id="draggable-windows-container" />
|
<Base id="draggable-windows-container" />
|
||||||
</Base>
|
</Base>
|
||||||
);
|
);
|
||||||
|
|||||||
+19
-3
@@ -1,3 +1,4 @@
|
|||||||
|
import { GetConfiguration } from '@nitrots/configuration';
|
||||||
import { configFileUrl, getClientMode, installSecureFetch } from './secure-assets';
|
import { configFileUrl, getClientMode, installSecureFetch } from './secure-assets';
|
||||||
|
|
||||||
const ensureMobileViewport = () =>
|
const ensureMobileViewport = () =>
|
||||||
@@ -15,7 +16,6 @@ const ensureMobileViewport = () =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
ensureMobileViewport();
|
ensureMobileViewport();
|
||||||
installSecureFetch();
|
|
||||||
|
|
||||||
const setBootDebug = (message: string) =>
|
const setBootDebug = (message: string) =>
|
||||||
{
|
{
|
||||||
@@ -30,8 +30,6 @@ const setBootDebug = (message: string) =>
|
|||||||
{}
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
setBootDebug('boot: secure fetch installed');
|
|
||||||
|
|
||||||
const deployBaseUrl = (): string =>
|
const deployBaseUrl = (): string =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -89,6 +87,9 @@ const loadClientMode = async () =>
|
|||||||
|
|
||||||
await loadClientMode();
|
await loadClientMode();
|
||||||
|
|
||||||
|
installSecureFetch();
|
||||||
|
setBootDebug('boot: secure fetch installed');
|
||||||
|
|
||||||
const search = new URLSearchParams(window.location.search);
|
const search = new URLSearchParams(window.location.search);
|
||||||
const clientMode = getClientMode();
|
const clientMode = getClientMode();
|
||||||
|
|
||||||
@@ -107,6 +108,21 @@ const clientMode = getClientMode();
|
|||||||
|
|
||||||
setBootDebug('boot: NitroConfig assigned');
|
setBootDebug('boot: NitroConfig assigned');
|
||||||
|
|
||||||
|
// Load renderer-config.json + ui-config.json BEFORE rendering React. Otherwise
|
||||||
|
// the first paint triggers a flood of "Missing configuration key" warnings for
|
||||||
|
// every key components read synchronously (asset.url, login.endpoint, …) until
|
||||||
|
// prepare()'s deferred init() finally lands. Doing it here makes the config
|
||||||
|
// already populated by the time index.tsx mounts <App/>.
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await GetConfiguration().init();
|
||||||
|
setBootDebug('boot: configuration init done');
|
||||||
|
}
|
||||||
|
catch(error)
|
||||||
|
{
|
||||||
|
setBootDebug(`boot: configuration init failed ${ error?.message || error }`);
|
||||||
|
}
|
||||||
|
|
||||||
import('./index')
|
import('./index')
|
||||||
.then(() => setBootDebug('boot: app bundle imported'))
|
.then(() => setBootDebug('boot: app bundle imported'))
|
||||||
.catch(error =>
|
.catch(error =>
|
||||||
|
|||||||
+16
-2
@@ -123,15 +123,19 @@ const REKEY_ENDPOINTS = new Set([
|
|||||||
'/api/auth/logout'
|
'/api/auth/logout'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
let clientModeCache: NitroClientMode | null = null;
|
||||||
|
|
||||||
export const getClientMode = (): NitroClientMode =>
|
export const getClientMode = (): NitroClientMode =>
|
||||||
{
|
{
|
||||||
|
if(clientModeCache) return clientModeCache;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const configured = (window as any).__nitroClientMode;
|
const configured = (window as any).__nitroClientMode;
|
||||||
|
|
||||||
if(configured && typeof configured === 'object')
|
if(configured && typeof configured === 'object')
|
||||||
{
|
{
|
||||||
return {
|
clientModeCache = {
|
||||||
distObfuscationEnabled: configured.distObfuscationEnabled !== false,
|
distObfuscationEnabled: configured.distObfuscationEnabled !== false,
|
||||||
secureAssetsEnabled: configured.secureAssetsEnabled !== false,
|
secureAssetsEnabled: configured.secureAssetsEnabled !== false,
|
||||||
secureApiEnabled: configured.secureApiEnabled !== false,
|
secureApiEnabled: configured.secureApiEnabled !== false,
|
||||||
@@ -139,12 +143,14 @@ export const getClientMode = (): NitroClientMode =>
|
|||||||
plainConfigBaseUrl: typeof configured.plainConfigBaseUrl === 'string' ? configured.plainConfigBaseUrl : '',
|
plainConfigBaseUrl: typeof configured.plainConfigBaseUrl === 'string' ? configured.plainConfigBaseUrl : '',
|
||||||
plainGamedataBaseUrl: typeof configured.plainGamedataBaseUrl === 'string' ? configured.plainGamedataBaseUrl : ''
|
plainGamedataBaseUrl: typeof configured.plainGamedataBaseUrl === 'string' ? configured.plainGamedataBaseUrl : ''
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return clientModeCache;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{}
|
{}
|
||||||
|
|
||||||
return { ...CLIENT_MODE_DEFAULTS };
|
return CLIENT_MODE_DEFAULTS;
|
||||||
};
|
};
|
||||||
|
|
||||||
const bytesToBase64 = (bytes: ArrayBuffer): string =>
|
const bytesToBase64 = (bytes: ArrayBuffer): string =>
|
||||||
@@ -489,6 +495,14 @@ export const installSecureFetch = (): void =>
|
|||||||
{
|
{
|
||||||
if(installed) return;
|
if(installed) return;
|
||||||
|
|
||||||
|
const mode = getClientMode();
|
||||||
|
|
||||||
|
if(!mode.secureAssetsEnabled && !mode.secureApiEnabled)
|
||||||
|
{
|
||||||
|
installed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
installed = true;
|
installed = true;
|
||||||
const nativeFetch = window.fetch.bind(window);
|
const nativeFetch = window.fetch.bind(window);
|
||||||
|
|
||||||
|
|||||||
+48
-2
@@ -2,11 +2,56 @@ import react from '@vitejs/plugin-react';
|
|||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
import sirv from 'sirv';
|
||||||
|
|
||||||
const legacyRendererRoot = resolve(__dirname, '..', 'renderer');
|
const legacyRendererRoot = resolve(__dirname, '..', 'renderer');
|
||||||
const currentRendererRoot = resolve(__dirname, '..', 'Nitro_Render_V3');
|
const currentRendererRoot = resolve(__dirname, '..', 'Nitro_Render_V3');
|
||||||
const rendererRoot = existsSync(currentRendererRoot) ? currentRendererRoot : legacyRendererRoot;
|
const rendererRoot = existsSync(currentRendererRoot) ? currentRendererRoot : legacyRendererRoot;
|
||||||
|
|
||||||
|
// Game assets live outside the repo, in a sibling directory next to Nitro-V3.
|
||||||
|
// They are NOT placed under public/ on purpose: with ~177k files a symlink
|
||||||
|
// under public/ makes chokidar try to install a watcher on each one and the
|
||||||
|
// dev server takes minutes to start on Windows. Serving them with a
|
||||||
|
// dedicated sirv middleware (below) bypasses chokidar entirely.
|
||||||
|
const nitroFilesRoot = resolve(__dirname, '..', 'Nitro-Files');
|
||||||
|
const nitroAssetsRoot = resolve(nitroFilesRoot, 'nitro-assets');
|
||||||
|
const swfRoot = resolve(nitroFilesRoot, 'swf');
|
||||||
|
|
||||||
|
const nitroAssetsServer = () => ({
|
||||||
|
name: 'nitro-assets-serve',
|
||||||
|
configureServer(server)
|
||||||
|
{
|
||||||
|
if(existsSync(nitroAssetsRoot))
|
||||||
|
{
|
||||||
|
server.middlewares.use('/nitro-assets', sirv(nitroAssetsRoot, { dev: true, etag: true, maxAge: 0 }));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
server.config.logger.warn(`[nitro-assets-serve] ${ nitroAssetsRoot } not found — /nitro-assets/* requests will 404.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(existsSync(swfRoot))
|
||||||
|
{
|
||||||
|
server.middlewares.use('/swf', sirv(swfRoot, { dev: true, etag: true, maxAge: 0 }));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
server.config.logger.warn(`[nitro-assets-serve] ${ swfRoot } not found — /swf/* requests will 404.`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
configurePreviewServer(server)
|
||||||
|
{
|
||||||
|
if(existsSync(nitroAssetsRoot))
|
||||||
|
{
|
||||||
|
server.middlewares.use('/nitro-assets', sirv(nitroAssetsRoot, { dev: false, etag: true }));
|
||||||
|
}
|
||||||
|
if(existsSync(swfRoot))
|
||||||
|
{
|
||||||
|
server.middlewares.use('/swf', sirv(swfRoot, { dev: false, etag: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if(!existsSync(rendererRoot))
|
if(!existsSync(rendererRoot))
|
||||||
{
|
{
|
||||||
// Fail fast with a useful message instead of waiting for Rolldown to
|
// Fail fast with a useful message instead of waiting for Rolldown to
|
||||||
@@ -36,7 +81,8 @@ export default defineConfig({
|
|||||||
[ 'babel-plugin-react-compiler', ReactCompilerConfig ]
|
[ 'babel-plugin-react-compiler', ReactCompilerConfig ]
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
nitroAssetsServer()
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
fs: {
|
fs: {
|
||||||
@@ -47,7 +93,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: process.env.AUTH_PROXY_TARGET || 'http://192.168.0.181:2096',
|
target: process.env.AUTH_PROXY_TARGET || 'http://localhost:2096',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -701,6 +701,11 @@
|
|||||||
"@parcel/watcher-win32-ia32" "2.5.6"
|
"@parcel/watcher-win32-ia32" "2.5.6"
|
||||||
"@parcel/watcher-win32-x64" "2.5.6"
|
"@parcel/watcher-win32-x64" "2.5.6"
|
||||||
|
|
||||||
|
"@polka/url@^1.0.0-next.24":
|
||||||
|
version "1.0.0-next.29"
|
||||||
|
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.29.tgz#5a40109a1ab5f84d6fd8fc928b19f367cbe7e7b1"
|
||||||
|
integrity sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==
|
||||||
|
|
||||||
"@radix-ui/number@1.1.1":
|
"@radix-ui/number@1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.1.tgz#7b2c9225fbf1b126539551f5985769d0048d9090"
|
resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.1.1.tgz#7b2c9225fbf1b126539551f5985769d0048d9090"
|
||||||
@@ -3096,6 +3101,11 @@ motion-utils@^12.36.0:
|
|||||||
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.36.0.tgz#cff2df2a28c3fe53a3de7e0103ba7f73ff7d77a7"
|
resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.36.0.tgz#cff2df2a28c3fe53a3de7e0103ba7f73ff7d77a7"
|
||||||
integrity sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==
|
integrity sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==
|
||||||
|
|
||||||
|
mrmime@^2.0.0:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.1.tgz#bc3e87f7987853a54c9850eeb1f1078cd44adddc"
|
||||||
|
integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==
|
||||||
|
|
||||||
ms@^2.1.3:
|
ms@^2.1.3:
|
||||||
version "2.1.3"
|
version "2.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||||
@@ -3665,6 +3675,15 @@ siginfo@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30"
|
resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30"
|
||||||
integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==
|
integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==
|
||||||
|
|
||||||
|
sirv@^3.0.2:
|
||||||
|
version "3.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/sirv/-/sirv-3.0.2.tgz#f775fccf10e22a40832684848d636346f41cd970"
|
||||||
|
integrity sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==
|
||||||
|
dependencies:
|
||||||
|
"@polka/url" "^1.0.0-next.24"
|
||||||
|
mrmime "^2.0.0"
|
||||||
|
totalist "^3.0.0"
|
||||||
|
|
||||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1:
|
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||||
@@ -3826,6 +3845,11 @@ tldts@^7.0.5:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tldts-core "^7.0.30"
|
tldts-core "^7.0.30"
|
||||||
|
|
||||||
|
totalist@^3.0.0:
|
||||||
|
version "3.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8"
|
||||||
|
integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==
|
||||||
|
|
||||||
tough-cookie@^6.0.1:
|
tough-cookie@^6.0.1:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-6.0.1.tgz#a495f833836609ed983c19bc65639cfbceb54c76"
|
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-6.0.1.tgz#a495f833836609ed983c19bc65639cfbceb54c76"
|
||||||
|
|||||||
Reference in New Issue
Block a user