Ctrl K
ComunidadGuías…

src/main.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// register vue composition api globally
import type { RouteLocationNormalized, Router } from 'vue-router'
import { Capacitor } from '@capacitor/core'
import { setupLayouts } from 'virtual:generated-layouts'
import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router'
import { routes } from 'vue-router/auto-routes'
import { isGuestRoutePath } from '~/constants/guestRoutes'
import { prefetchSsoEnforcement } from '~/modules/sso-enforcement'
import { getLocalConfig } from '~/services/console'
import { installDeepLinkHandler } from '~/services/deepLinks'
import { isDocumentNavigationPending, replaceDocument } from '~/services/documentNavigation'
import { getNativeExternalPurchaseRedirect, isNativeAppStoreContext, isNativeExternalPurchaseRestrictedPath } from '~/services/nativeCompliance'
import { posthogLoader } from '~/services/posthog'
import { captureConsoleAttributionFromLocation } from '~/services/signupAttribution'
import { getErrorMessage, isComponentResolutionErrorMessage, isKnownCrawlerNoiseErrorMessage, isStaleAssetErrorMessage } from '~/services/staleAssetErrors'
import { installConsoleImageFallback } from '~/services/storage'
import { getAllowedConfirmationHosts, resolveConfirmationUrl } from '~/utils/safeRedirect'
import App from './App.vue'
import { defaultApiHost, getRemoteConfig } from './services/console'
import { resolveConsoleApiBase } from './services/consoleAuth'
import '@fontsource-variable/inter'
// your custom styles here
import './styles/style.css'

// Handle chunk load errors (stale chunks after deployment)
// When a new version is deployed, old chunk URLs return 404/HTML instead of JS
const CHUNK_RELOAD_TIMESTAMP_KEY = 'capgo_chunk_reload_timestamp'
const CHUNK_RELOAD_TOAST_KEY = 'capgo_chunk_reload_toast'
const CHUNK_RELOAD_COOLDOWN_MS = 30_000

function getChunkReloadTimestamp(): number | null {
  try {
    const storedValue = sessionStorage.getItem(CHUNK_RELOAD_TIMESTAMP_KEY)
    if (!storedValue)
      return null

    const timestamp = Number.parseInt(storedValue, 10)
    return Number.isFinite(timestamp) ? timestamp : null
  }
  catch {
    return null
  }
}

function setChunkReloadTimestamp(): void {
  try {
    sessionStorage.setItem(CHUNK_RELOAD_TIMESTAMP_KEY, String(Date.now()))
  }
  catch {
    // Ignore storage access failures and still let the reload happen.
  }
}

function hasChunkReloadToastPending(): boolean {
  try {
    return sessionStorage.getItem(CHUNK_RELOAD_TOAST_KEY) === 'true'
  }
  catch {
    return false
  }
}

function setChunkReloadToastPending(): void {
  try {
    sessionStorage.setItem(CHUNK_RELOAD_TOAST_KEY, 'true')
  }
  catch {
    // Ignore storage access failures and still let the reload happen.
  }
}

function clearChunkReloadToastPending(): void {
  try {
    sessionStorage.removeItem(CHUNK_RELOAD_TOAST_KEY)
  }
  catch {
    // Ignore storage access failures during cleanup.
  }
}

// `targetPath` is the route the user was navigating to when the chunk failed.
// A bare reload restores the URL the browser is still on (the page the user is
// leaving), so it silently throws the navigation away. When we know the target
// we navigate there instead, so the user lands where they asked to go.
function handleChunkError(message: string, targetPath?: string) {
  if (isDocumentNavigationPending())
    return

  const previousReload = getChunkReloadTimestamp()
  if (previousReload && Date.now() - previousReload < CHUNK_RELOAD_COOLDOWN_MS) {
    console.warn('Chunk load error detected again after a recent reload, skipping automatic reload.', message)
    return
  }

  console.warn('Chunk load error detected, reloading page...', message)
  setChunkReloadTimestamp()
  setChunkReloadToastPending()
  if (targetPath)
    window.location.assign(targetPath)
  else
    window.location.reload()
}

window.addEventListener('error', (event) => {
  if (isStaleAssetErrorMessage(event.message)) {
    event.preventDefault()
    event.stopImmediatePropagation()
    handleChunkError(event.message)
    return
  }

  if (isKnownCrawlerNoiseErrorMessage(event.message)) {
    event.preventDefault()
    event.stopImmediatePropagation()
  }
}, true)

// Also handle unhandled promise rejections for dynamic imports
window.addEventListener('unhandledrejection', (event) => {
  const message = getErrorMessage(event.reason) ?? String(event.reason)
  if (isStaleAssetErrorMessage(message)) {
    event.preventDefault()
    event.stopImmediatePropagation()
    handleChunkError(message)
    return
  }

  if (isKnownCrawlerNoiseErrorMessage(message)) {
    event.preventDefault()
    event.stopImmediatePropagation()
  }
})

window.addEventListener('vite:preloadError', (event) => {
  const preloadEvent = event as Event & { payload?: unknown, detail?: unknown }
  const message = getErrorMessage(preloadEvent.payload)
    ?? getErrorMessage(preloadEvent.detail)
    ?? 'Vite preload error'
  if (!isStaleAssetErrorMessage(message))
    return
  // Deliberately do NOT call event.preventDefault(): Vite's preload helper only
  // rethrows the underlying import failure when the default is not prevented.
  // Preventing it makes `__vitePreload` resolve with `undefined`, so a lazy route
  // resolves to a falsy component and vue-router throws "Couldn't resolve
  // component" instead of the real chunk error. Letting it rethrow keeps the
  // rejection matching STALE_ASSET_ERROR_PATTERNS, so it still gets the reload
  // (below) and the PostHog suppression this handler was written to provide.
  handleChunkError(message)
})

function redirectAppPath(suffix: string) {
  return (to: RouteLocationNormalized) => ({
    path: `/app/${encodeURIComponent(String((to.params as { app: string }).app))}${suffix}`,
    query: to.query,
    hash: to.hash,
  })
}

getRemoteConfig()
const app = createApp(App)
installConsoleImageFallback()
// The updater only does work inside the native shell; keep it out of the web entry chunk.
async function notifyNativeAppReady() {
  const { CapacitorUpdater } = await import('@capgo/capacitor-updater')
  await CapacitorUpdater.notifyAppReady()
}
if (Capacitor.isNativePlatform())
  notifyNativeAppReady().catch(error => console.error('Cannot notify the updater that the app is ready', error))
console.log(`Capgo Version : "${import.meta.env.VITE_APP_VERSION}"`)
// setup up pages with layouts
const newRoutes = routes.map((route) => {
  if (isGuestRoutePath(route.path)) {
    route.meta ??= {}
    route.meta.layout = 'naked'
  }
  else {
    route.meta ??= {}
    route.meta.middleware = 'auth'
  }
  return route
})
const router = createRouter({
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition)
      return savedPosition
    if (to.hash)
      return { el: to.hash, top: 0 }
    if (from && to.path === from.path && (to.hash || '') === (from.hash || ''))
      return false
    return { top: 0 }
  },
  routes: [
    { path: '/', redirect: '/login' },
    // Canonical apps list route
    { path: '/app', redirect: '/apps' },
    { path: '/settings/plans', redirect: '/settings/organization/plans' },
    { path: '/settings/usage', redirect: '/settings/organization/usage' },
    { path: '/.well-known/change-password', redirect: '/settings/account/change-password' },
    { path: '/settings/change-password', redirect: '/settings/account/change-password' },
    { path: '/settings/changepassword', redirect: '/settings/account/change-password' },
    { path: '/settings/notifications', redirect: '/settings/account/notifications' },
    { path: '/dashboard/settings/organization/plans', redirect: '/settings/organization/plans' },
    { path: '/dashboard/settings/organization/usage', redirect: '/settings/organization/usage' },
    { path: '/p/:package', redirect: to => `/app/${(to.params as { package: string }).package}` },
    {
      path: '/p/:package/logs',
      redirect: (to) => {
        const { tab: _, ...query } = to.query
        return {
          path: `/app/${encodeURIComponent(String((to.params as { package: string }).package))}/observe/logs`,
          query,
          hash: to.hash,
        }
      },
    },
    { path: '/dashboard/apikeys', redirect: '/apikeys' },
    { path: '/dashboard/settings/account', redirect: '/settings/account' },
    { path: '/dashboard/settings/change-password', redirect: '/settings/account/change-password' },
    { path: '/dashboard/settings/notifications', redirect: '/settings/notifications' },
    { path: '/dashboard/settings/organization/general', redirect: '/settings/organization/' },
    { path: '/dashboard/settings/organization/members', redirect: '/settings/organization/members' },
    { path: '/dashboard/settings/organization/plans', redirect: '/settings/organization/plans' },
    { path: '/dashboard/settings/organization/usage', redirect: '/settings/organization/usage' },
    { path: '/app/p/:package', redirect: to => `/app/${(to.params as { package: string }).package}` },
    { path: '/app/p/:package/bundles', redirect: to => `/app/${(to.params as { package: string }).package}` },
    { path: '/app/p/:package/channels', redirect: to => `/app/${(to.params as { package: string }).package}` },
    { path: '/app/p/:package/devices', redirect: to => `/app/${(to.params as { package: string }).package}` },
    {
      path: '/app/p/:package/logs',
      redirect: (to) => {
        const { tab: _, ...query } = to.query
        return {
          path: `/app/${encodeURIComponent(String((to.params as { package: string }).package))}/observe/logs`,
          query,
          hash: to.hash,
        }
      },
    },
    {
      path: '/app/package/:package',
      redirect: to => ({
        path: `/app/${encodeURIComponent(String((to.params as { package: string }).package))}`,
        query: to.query,
        hash: to.hash,
      }),
    },
    {
      path: '/app/package/:package/settings',
      redirect: to => ({
        path: `/app/${encodeURIComponent(String((to.params as { package: string }).package))}/settings`,
        query: to.query,
        hash: to.hash,
      }),
    },
    // Removed mobile modules / plugins pages
    { path: '/app/modules', redirect: '/apps' },
    { path: '/app/modules_test', redirect: '/apps' },
    { path: '/app/plugins', redirect: '/apps' },
    // Legacy app tab URLs after settings/observe revamp
    { path: '/app/:app/info', redirect: redirectAppPath('/settings') },
    { path: '/app/:app/access', redirect: redirectAppPath('/settings/access') },
    { path: '/app/:app/logs/insights', redirect: redirectAppPath('/observe/errors') },
    { path: '/app/:app/logs', redirect: redirectAppPath('/observe/logs') },
    { path: '/app/:app/compatibility', redirect: redirectAppPath('/observe/compatibility') },
    { path: '/app/:app/observe', redirect: redirectAppPath('') },
    // Legacy overview sub-tabs and Observe tabs after the overview/observe regrouping
    { path: '/app/:app/live', redirect: redirectAppPath('') },
    { path: '/app/:app/installs', redirect: redirectAppPath('') },
    { path: '/app/:app/active-bundle', redirect: redirectAppPath('') },
    { path: '/app/:app/native', redirect: redirectAppPath('/observe/native') },
    { path: '/app/:app/observe/releases', redirect: redirectAppPath('') },
    { path: '/app/:app/observe/updater', redirect: redirectAppPath('/observe/errors') },
    { path: '/app/:app/observe/plugins', redirect: to => ({ ...redirectAppPath('/observe/compatibility')(to), query: { ...to.query, view: 'plugins' } }) },
    ...setupLayouts(newRoutes),
  ],
  history: createWebHistory(import.meta.env.BASE_URL),
})

// Recover from lazy-route load failures that slip past the vite:preloadError
// handler (e.g. a navigation that races the reload). vue-router surfaces these
// as "Couldn't resolve component" once a stale chunk fails to import, so treat
// them as chunk errors and trigger the same reload instead of leaving the user
// on a dead route.
router.onError((error, to) => {
  const message = getErrorMessage(error) ?? String(error)
  if (isStaleAssetErrorMessage(message) || isComponentResolutionErrorMessage(message))
    handleChunkError(message, to?.fullPath)
})

// vue-router only fetches lazy route chunks after every beforeEach guard resolved, and the auth
// guard waits on the API. Start the chunk downloads and the SSO enforcement request now so they
// run in parallel with it; the router's own import() later reuses the same module request.
router.beforeEach((to) => {
  prefetchSsoEnforcement(to.path)
  for (const record of to.matched) {
    for (const component of Object.values(record.components ?? {})) {
      if (typeof component === 'function' && !('displayName' in component) && !('props' in component))
        void (component as () => Promise<unknown>)()?.catch?.(() => {})
    }
  }
})

router.beforeEach((to, from, next) => {
  if (to.path === '/confirm-signup' || to.path === '/confirm-signup/') {
    try {
      const confirmationUrl = resolveConfirmationUrl(to.query, {
        allowedHosts: getAllowedConfirmationHosts(),
        allowLocalDev: import.meta.env.DEV,
        apiHost: resolveConsoleApiBase(defaultApiHost),
      })
      if (confirmationUrl) {
        replaceDocument(confirmationUrl)
        return next(false)
      }
    }
    catch {
      // Let the confirmation page display a navigation error.
    }
  }

  if (isNativeAppStoreContext() && isNativeExternalPurchaseRestrictedPath(to.path)) {
    return next(getNativeExternalPurchaseRedirect(to.path))
  }

  if (to.path.startsWith('/app/') && to.query.tab) {
    const tab = to.query.tab as string
    const newPath = to.path.endsWith('/') ? `${to.path}${tab}` : `${to.path}/${tab}`
    const { tab: _, ...newQuery } = to.query
    return next({ path: newPath, query: newQuery, hash: to.hash })
  }
  next()
})

const config = getLocalConfig()
posthogLoader(config.apiHost)
captureConsoleAttributionFromLocation()

// install all modules under `modules/`
type UserModule = (ctx: { app: typeof app, router: Router }) => void | Promise<void>

// Modules may return a promise (i18n loads the user's locale) that must settle before mount.
// allSettled: a failing module must never keep the app from mounting.
const moduleSetup = Promise.allSettled(Object.values(import.meta.glob<{ install: UserModule }>('./modules/*.ts', { eager: true }))
  .map(i => i.install?.({ app, router })))

app.use(router)
void installDeepLinkHandler(router)

void Promise.all([router.isReady(), moduleSetup]).then(async () => {
  if (isDocumentNavigationPending())
    return

  app.mount('#app')

  // Wait for vue-sonner component to be mounted
  setTimeout(async () => {
    if (isDocumentNavigationPending())
      return

    const key = hasChunkReloadToastPending()
    console.log('Checking for chunk reload toast...', key)
    // Show toast if we just reloaded due to chunk error
    if (key) {
      clearChunkReloadToastPending()
      const { toast } = await import('vue-sonner')
      toast.info('App updated! Page was refreshed to load the latest version.')
    }
  }, 500)
})