/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format */ import { getLogger, getStringFromErrorLike, setLoggerInstance, } from 'flipper-common'; import {init as initLogger} from './fb-stubs/Logger'; import {initializeRenderHost} from './initializeRenderHost'; import {createFlipperServer, FlipperServerState} from 'flipper-server-client'; const loadingContainer = document.getElementById('loading'); if (loadingContainer) { loadingContainer.innerText = 'Loading...'; } let cachedFile: {name: string; data: string} | undefined; let cachedDeepLinkURL: string | undefined; const logger = initLogger(); async function start() { /** * The following is used to ensure only one instance of Flipper is running at a time. * The event will not be fired for the current tab. */ window.addEventListener('storage', function (event) { if (event.key === 'flipper-kill-window') { window.close(); } }); // @ts-ignore electronRequire = function (path: string) { console.error( `[decapitate] Tried to electronRequire ${path}, this module is not available in the browser and will be stubbed`, ); return { default: {}, }; }; setLoggerInstance(logger); const params = new URL(location.href).searchParams; const tokenProvider = async () => { const providerParams = new URL(location.href).searchParams; let token = providerParams.get('token'); if (!token) { console.info( '[flipper-client][ui-browser] Get token from manifest instead', ); try { const manifestResponse = await fetch('manifest.json'); const manifest = await manifestResponse.json(); token = manifest.token; } catch (e) { console.warn( '[flipper-client][ui-browser] Failed to get token from manifest. Error:', e.message, ); } } getLogger().info( '[flipper-client][ui-browser] Token is available: ', token?.length != 0, ); return token; }; const openPlugin = params.get('open-plugin'); if (openPlugin) { function removePrefix(input: string, prefix: string): string { const regex = new RegExp(`^${prefix}+`); return input.replace(regex, ''); } const url = new URL(openPlugin); const maybeParams = removePrefix(url.pathname, '/'); const params = new URLSearchParams(maybeParams); const deeplinkURL = new URL('flipper://open-plugin'); deeplinkURL.search = params.toString(); cachedDeepLinkURL = deeplinkURL.toString(); } getLogger().info('[flipper-client][ui-browser] Create WS client'); const flipperServer = await createFlipperServer( location.hostname, parseInt(location.port, 10), tokenProvider, (state: FlipperServerState) => { switch (state) { case FlipperServerState.CONNECTING: getLogger().info('[flipper-client] Connecting to server'); window.flipperShowMessage?.('Connecting to server...'); break; case FlipperServerState.CONNECTED: getLogger().info( '[flipper-client] Connection established with server', ); window.flipperHideMessage?.(); break; case FlipperServerState.DISCONNECTED: getLogger().info('[flipper-client] Disconnected from server'); window.flipperShowMessage?.('Waiting for server...'); break; } }, ); getLogger().info('[flipper-client][ui-browser] WS client connected'); flipperServer.on('server-log', (logEntry) => { getLogger()[logEntry.type]( `[${logEntry.namespace}] (${new Date( logEntry.time, ).toLocaleTimeString()}): ${logEntry.msg}`, ); }); getLogger().info( '[flipper-client][ui-browser] Waiting for server connection', ); await flipperServer.connect(); getLogger().info( '[flipper-client][ui-browser] Connected to server, get configuration', ); const flipperServerConfig = await flipperServer.exec('get-config'); getLogger().info( '[flipper-client][ui-browser] Configuration obtained, initialise render host', ); initializeRenderHost(flipperServer, flipperServerConfig); initializePWA(); // @ts-ignore // eslint-disable-next-line import/no-commonjs require('flipper-ui-core').startFlipperDesktop(flipperServer); window.flipperHideMessage?.(); /** * At this stage, the current client has established a connection with the server. * So, it is safe to 'set' into local storage so that other clients close. */ localStorage.setItem('flipper-kill-window', Date.now().toString()); getLogger().info('[flipper-client][ui-browser] UI initialised'); logger.track('success-rate', 'flipper-ui-browser-started', {value: 1}); } start().catch((e) => { getLogger().error('Failed to start flipper-ui-browser', e); logger.track('success-rate', 'flipper-ui-browser-started', { value: 0, error: getStringFromErrorLike(e), pwa: window.matchMedia('(display-mode: standalone)').matches, }); window.flipperShowMessage?.('Failed to start UI with error: ' + e); }); async function initializePWA() { getLogger().info('[PWA] Initialization'); let rehydrated = false; const openFileIfAny = () => { if (!cachedFile || !rehydrated) { return; } window.dispatchEvent( new CustomEvent('open-flipper-file', { detail: [cachedFile.name, cachedFile.data], }), ); cachedFile = undefined; }; const openURLIfAny = () => { if (!cachedDeepLinkURL || !rehydrated) { return; } window.dispatchEvent( new CustomEvent('flipper-protocol-handler', { detail: [cachedDeepLinkURL], }), ); cachedDeepLinkURL = undefined; }; if ('serviceWorker' in navigator) { navigator.serviceWorker .register('/service-worker.js') .then(() => { getLogger().info('[PWA] Service Worker has been registered'); }) .catch((e) => { getLogger().error('[PWA] failed to register Service Worker', e); }); } if ('launchQueue' in window) { getLogger().debug('[PWA] File Handling API is supported'); // @ts-ignore window.launchQueue.setConsumer(async (launchParams) => { if (!launchParams || !launchParams.files) { return; } getLogger().debug('[PWA] Attempt to to open a file'); for (const file of launchParams.files) { const blob = await file.getFile(); blob.handle = file; const data = await blob.text(); const name = file.name; cachedFile = {name, data}; openFileIfAny(); } }); } else { console.warn('[PWA] File Handling API is not supported'); } getLogger().debug('[PWA] Add before install prompt listener'); window.addEventListener('beforeinstallprompt', (e) => { // Prevent Chrome 67 and earlier from automatically showing the prompt. e.preventDefault(); // Stash the event so it can be triggered later. // @ts-ignore global.PWAppInstallationEvent = e; getLogger().info('[PWA] Installation event has been captured'); }); window.addEventListener('storeRehydrated', () => { getLogger().info('[PWA] Store is rehydrated'); rehydrated = true; openFileIfAny(); openURLIfAny(); }); }