Ctrl K
ComunidadGuías…

nuxt.config.ts

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

Archivo original ↗Preguntar a KiBot
import { defineNuxtConfig } from 'nuxt/config';
import tailwindcss from '@tailwindcss/vite';
import { brandingEnvDefaults, brandingLockedDefault, config } from './config';
import path from 'path';
import { inMemoryCacheOptions } from './cache';
import { DORMANT_AUTH0_CONFIG } from './utils/auth0RuntimeConfig';
import { getAuthStorageConfig } from './utils/authStorageConfig';
import { resolveImageProvider } from './utils/imageProvider';

const isMockedE2E = process.env.VITE_E2E_MOCK_MODE === 'true';
const browserGraphqlUrl = config?.graphqlUrl || 'http://localhost:4000';
const serverGraphqlUrl =
  process.env.NUXT_BACKEND_GRAPHQL_URL || browserGraphqlUrl;
const frontendGraphqlProxyUrl = '/api/graphql';
const imageProvider = resolveImageProvider({
  nitroPreset: process.env.NITRO_PRESET,
  vercel: process.env.VERCEL,
});
const runtimeGraphqlFetch: typeof globalThis.fetch = async (input, init) => {
  const runtimeBackendUrl =
    typeof window === 'undefined'
      ? process.env.NUXT_BACKEND_GRAPHQL_URL?.trim()
      : '';
  const target =
    typeof window === 'undefined' ? runtimeBackendUrl || input : input;
  const startedAt = performance.now();
  const response = await globalThis.fetch(target, init);

  if (typeof window === 'undefined') {
    // Nuxt serializes this callback into the generated Apollo plugin, so it
    // must remain self-contained rather than referring to imported helpers.
    const configuredThreshold = Number(
      process.env.NUXT_GRAPHQL_SLOW_REQUEST_MS
    );
    const thresholdMs =
      Number.isFinite(configuredThreshold) && configuredThreshold >= 0
        ? configuredThreshold
        : 500;
    const durationMs = performance.now() - startedAt;

    if (durationMs >= thresholdMs) {
      let operationName = 'unknown';
      if (typeof init?.body === 'string') {
        try {
          const parsedBody = JSON.parse(init.body) as {
            operationName?: unknown;
          };
          operationName =
            typeof parsedBody.operationName === 'string' &&
            parsedBody.operationName
              ? parsedBody.operationName
              : 'anonymous';
        } catch {
          // Keep the fallback name; never log raw request bodies or variables.
        }
      }

      console.info(
        `[graphql-timing] ${JSON.stringify({
          operationName,
          durationMs: Math.round(durationMs * 10) / 10,
          status: response.status,
          serverTiming: response.headers.get('server-timing'),
        })}`
      );
    }
  }

  return response;
};
const ignoredDevWatchPatterns = [
  '**/.stateful/**',
  '**/.claude/**',
  '**/.agents/**',
  '**/.vercel/**',
  '**/.output/**',
  '**/coverage/**',
  '**/coverage-combined/**',
  '**/playwright-report/**',
  '**/test-results/**',
];
const manualChunkGroups: Record<string, string[]> = {
  'vue-libs': ['vue', 'vue-router', 'pinia'],
  apollo: ['@apollo/client', '@vue/apollo-composable'],
  'date-libs': ['luxon'],
  'map-libs': ['@googlemaps/js-api-loader'],
};
const ignoredNuxtPaths = [
  '.stateful',
  '.stateful/**',
  '.claude',
  '.claude/**',
  '.agents',
  '.agents/**',
  '.vercel',
  '.vercel/**',
  '.output',
  '.output/**',
  'coverage',
  'coverage/**',
  'coverage-combined',
  'coverage-combined/**',
  'playwright-report',
  'playwright-report/**',
  'test-results',
  'test-results/**',
];

export default defineNuxtConfig({
  srcDir: '.',
  ignore: ignoredNuxtPaths,
  app: {
    head: {
      title: config.serverDisplayName,
      meta: [
        { charset: 'utf-8' },
        { name: 'viewport', content: 'width=device-width, initial-scale=1' },
        {
          name: 'description',
          content: `Welcome to ${config.serverDisplayName}`,
        },
        { name: 'color-scheme', content: 'dark light' },
      ],
      htmlAttrs: {
        lang: 'en',
        // The `dark` class is applied per-request by the uiStore (from the
        // `theme` cookie) during SSR, so it is NOT hardcoded here — otherwise
        // light-mode users would get a dark server render and a flash on load.
        class: 'dark-mode-ready',
      },
    },
  },
  vue: {
    compilerOptions: {
      whitespace: 'preserve',
      isCustomElement: (tag) => tag === 'model-viewer',
    },
  },
  experimental: {
    payloadExtraction: false,
    defaults: {
      // Default NuxtLink prefetch triggers on viewport visibility, so list
      // pages (discussions, events, images) speculatively download the route
      // chunks of every link on screen. Prefetch on hover/focus instead: links
      // the user actually aims at still load instantly, but we stop pulling
      // megabytes of route chunks for links that are merely visible.
      nuxtLink: {
        prefetchOn: { visibility: false, interaction: true },
      },
    },
  },
  compatibilityDate: '2024-04-03',
  components: true,
  css: ['@/assets/css/index.css'],
  devtools: { enabled: true },
  imports: {
    autoImport: true,
  },
  modules: [
    '@sentry/nuxt/module',
    // Server-session auth: registers /auth/login, /auth/logout,
    // /auth/callback, /auth/backchannel-logout. Config is read from the private
    // `runtimeConfig.auth0` block below (NUXT_AUTH0_* envs).
    [
      '@auth0/auth0-nuxt',
      {
        mountRoutes: true,
        // SPIKE Phase 2: use a server-side session store (small cookie holds
        // only a session id). Keeping the module in every build lets one
        // container select Auth0 or local-dev auth through runtime config.
        sessionStoreFactoryPath: '~/server/utils/session-store-factory.ts',
      },
    ],
    [
      '@nuxtjs/apollo',
      {
        clients: {
          default: {
            // The custom fetch keeps this module's generated Apollo client
            // reusable: SSR reads NUXT_BACKEND_GRAPHQL_URL at process startup,
            // while browser requests use the same-origin proxy below. The
            // endpoint value is only a backward-compatible build fallback.
            httpEndpoint: serverGraphqlUrl,
            browserHttpEndpoint: frontendGraphqlProxyUrl,
            httpLinkOptions: {
              fetch: runtimeGraphqlFetch,
            },
            // @nuxtjs/apollo attaches localStorage['token'] as the Bearer
            // (tokenStorage: 'localStorage'). plugins/apollo-auth.client.ts keeps
            // that key in sync with the server-session access token. (The old
            // custom `apolloLink` here was dead config — this module version
            // builds its own link and ignored it — so it was removed.)
            tokenName: 'token',
            tokenStorage: 'localStorage',
            inMemoryCacheOptions,

            devtools: { enabled: process.env.NODE_ENV === 'development' },
            defaultOptions: {
              watchQuery: {
                errorPolicy: 'all',
                notifyOnNetworkStatusChange: true,
                fetchPolicy: 'cache-first',
              },
              query: {
                errorPolicy: 'all',
                notifyOnNetworkStatusChange: true,
                fetchPolicy: 'cache-first',
              },
              mutation: {
                errorPolicy: 'all',
              },
            },
          },
        },
      },
    ],
    // Add image optimization
    [
      '@nuxt/image',
      {
        // Image quality options
        quality: 80,
        // Use WebP and AVIF formats where supported
        format: ['webp', 'avif', 'jpg', 'png'],
        // Vercel's native optimizer avoids packaging IPX and sharp into the
        // server function. Other deployment targets keep the self-hosted IPX
        // endpoint so their image behavior is unchanged.
        provider: imageProvider,
        // Responsive image breakpoints
        screens: {
          xs: 320,
          sm: 640,
          md: 768,
          lg: 1024,
          xl: 1280,
          xxl: 1536,
          '2xl': 1536,
        },
        // Default image optimization options
        modifiers: {
          format: 'webp',
          quality: 80,
          width: 'auto',
          height: 'auto',
        },
        // Domains to allow for remote images
        domains: ['storage.googleapis.com'],
        // Adjust placeholder behavior
        placeholder: {
          size: 10,
        },
        // Presets for common image types
        presets: {
          avatar: {
            modifiers: {
              format: 'webp',
              width: 50,
              height: 50,
            },
          },
          thumbnail: {
            modifiers: {
              format: 'webp',
              width: 320,
              height: 180,
            },
          },
          cover: {
            modifiers: {
              format: 'webp',
              width: 1200,
              height: 630,
            },
          },
        },
      },
    ],
    // Light/dark mode is handled by the uiStore (single source of truth):
    // it reads the `theme` cookie on the server to set the initial <html> class
    // and toggles it on the client. @nuxtjs/color-mode was unused, so it was
    // removed to eliminate a competing theme system. Tailwind's `dark` variant
    // is configured against the same class in assets/css/index.css.
    [
      '@nuxtjs/google-fonts',
      {
        families: {
          // Inter is self-hosted in assets/css/index.css, and the public UI
          // only uses Montserrat for the wordmark. Limiting the remote font
          // request keeps first paint from competing with dozens of unused font
          // variants on public routes.
          Montserrat: [600],
        },
        subsets: ['latin'],
        display: 'swap',
        // Keep font loading non-blocking. `display: swap` + `preconnect` are
        // enough for first paint here; explicit font prefetch competes with the
        // route's critical CSS/JS on public pages.
        prefetch: false,
        preconnect: true,
      },
    ],
  ],
  sentry: {
    // Build-time module options ONLY (source maps upload). Runtime options such
    // as `dsn`, `environment`, `integrations`, and sample rates belong in
    // `Sentry.init()` inside sentry.client.config.ts / sentry.server.config.ts —
    // they are ignored if placed here.
    org: 'none-y1x',
    project: 'javascript-nuxt',
    authToken: process.env.VITE_SENTRY_AUTH_TOKEN,
  },
  nitro: {
    preset: process.env.NITRO_PRESET || 'vercel',
    // Nuxt bundles pages, middleware, and API routes into one Vercel Function.
    // The GraphQL proxy can legitimately wait for a pre-download security scan,
    // which exceeds Vercel's 15-second non-Fluid default on a cold scanner.
    vercel: {
      functions: {
        maxDuration: 60,
      },
    },
    // `sanitize-html` (used by composables/useMarkdownRenderer.ts) is CommonJS
    // and does `require('htmlparser2')`. When Nitro externalizes it, the Vercel
    // serverless function performs that require at runtime — which crashes with
    // "require() of ES Module .../htmlparser2/dist/index.js not supported"
    // whenever the deployed htmlparser2 resolves to its ESM entry (e.g. a stale
    // copy served by Vercel's build cache). Inlining sanitize-html bundles it
    // (and htmlparser2) at build time, so there is no runtime require of
    // htmlparser2 — the server is immune regardless of which htmlparser2 the
    // dependency install yields.
    externals: {
      inline: ['sanitize-html', 'htmlparser2'],
    },
    ignore: ignoredDevWatchPatterns,
    watchOptions: {
      ignoreInitial: true,
      ignored: ignoredDevWatchPatterns,
    },
    // Enable CDN caching
    cdn: true,
    // Persistent store for @auth0/auth0-nuxt server sessions
    // (server/utils/session-store-factory.ts uses useStorage('auth0Sessions')).
    // The default in-memory store is wiped on every server restart, leaving the
    // browser with a stale session-id cookie whose id is no longer in the store;
    // StatefulStateStore then DELETES the cookie on that miss and the user is
    // silently logged out. A persistent, shared store survives restarts and (on
    // serverless) is shared across function instances, avoiding that cascade.
    //
    // Vercel uses Upstash Redis over its REST API so sessions are shared across
    // function instances. The official node-server container keeps Auth0
    // sessions under /app/data and uses a bounded in-process profile cache,
    // avoiding an external Redis service for one frontend replica.
    storage: getAuthStorageConfig({
      nitroPreset: process.env.NITRO_PRESET,
      dataDir: process.env.MULTIFORUM_DATA_DIR,
    }),
    // Local dev keeps filesystem-backed mounts (no Upstash creds needed) that
    // still survive `nuxt dev` restarts. devStorage overrides the production
    // mounts above only during development.
    devStorage: {
      auth0Sessions: {
        driver: 'fs',
        base: './.auth0-sessions',
      },
      authProfileCache: {
        driver: 'lru-cache',
        max: 1000,
        ttl: 60 * 60 * 1000,
      },
    },
    // Enable server-side caching
    routeRules: {
      // Forum detail pages (discussions / events / downloads) are NOT cached.
      // Since the server-session migration, SSR is AUTH-AWARE: the server
      // personalizes the HTML from the user's session (edit/owner controls,
      // vote state, nav). A shared ISR/edge cache would capture ONE user's
      // personalized render and serve it to everyone — producing hydration
      // mismatches (cached auth state != the visitor's real auth state, causing
      // the page to blank and re-render) and leaking that user's auth state to
      // others. These must render on demand per request.
      //
      // (Before the migration SSR was anonymous, so ISR was safe; that premise
      // no longer holds. If per-page caching is wanted back, it would need to be
      // anonymous-only — e.g. bypass the cache whenever a session cookie is
      // present — not a blanket ISR rule.)
      // Pipeline history is interactive operational data, not SEO content.
      // Render it client-side so the detail page's SSR query waterfall cannot
      // exhaust the Vercel function timeout before pipeline data is requested.
      '/forums/*/downloads/*/pipelines': { ssr: false },
      // SPIKE Phase 2: auth endpoints must NOT be route-cached. Nitro's route
      // cache serves shared, cookie-independent responses, so /api/auth/token
      // was reaching its handler with NO cookies (no session → null token →
      // "You must be logged in"). A more specific rule wins in Nitro, so this
      // disables caching for the auth routes only.
      '/api/auth/**': { cache: false },
      '/api/session/**': { cache: false },
      // GraphQL POST responses can be user-specific and proxyRequest returns a
      // streaming response that Nitro's route cache cannot serialize.
      '/api/graphql': { cache: false },
      // Cache other API routes
      '/api/**': {
        cache: {
          // Let middleware handle specific cache times
          headers: ['cache-control'],
        },
      },
      // Cache static assets
      '/_nuxt/**': {
        headers: {
          'cache-control': 'public, max-age=31536000, immutable',
        },
      },
      // Cache public assets
      '/assets/**': {
        headers: {
          'cache-control': 'public, max-age=31536000, immutable',
        },
      },
    },
  },
  plugins: [
    { src: '@/plugins/00.runtime-instance-config', mode: 'all' },
    { src: '@/plugins/pinia', mode: 'all' },
    { src: '@/plugins/click-outside', mode: 'all' },
    { src: '@/plugins/accented.client', mode: 'client' },
    { src: '@/plugins/test-auth.client', mode: 'client' },
  ],
  runtimeConfig: {
    // Server-only GraphQL URL. Docker deployments override it at runtime to
    // reach the backend over the private Compose network; browsers use the
    // same-origin /api/graphql proxy instead.
    backendGraphqlUrl: serverGraphqlUrl,
    // Optional explicit backend token URL for local development auth. When it
    // is empty, the server derives /auth/local-dev/token from the server-side
    // GraphQL URL above, then falls back to the public browser URL.
    localAuthTokenEndpoint: '',
    // SPIKE (auth0-nuxt server-session migration): server-only Auth0 config.
    // These are overridden by NUXT_AUTH0_* env vars at runtime (see
    // .env.production.example). clientSecret + sessionSecret mean this is a
    // CONFIDENTIAL "Regular Web Application" in Auth0 — a different app type
    // than the current SPA.
    auth0: {
      // The Auth0 module validates these fields even when local-dev auth is
      // selected. Inert defaults let one image boot in either mode; the Nitro
      // startup validator rejects them whenever authProvider is actually
      // `auth0`, so production still requires real NUXT_AUTH0_* secrets.
      domain: DORMANT_AUTH0_CONFIG.domain,
      clientId: DORMANT_AUTH0_CONFIG.clientId,
      clientSecret: DORMANT_AUTH0_CONFIG.clientSecret,
      sessionSecret: DORMANT_AUTH0_CONFIG.sessionSecret,
      appBaseUrl: 'http://localhost:3000',
      audience: '',
      // Spread by the SDK into the session store's cookie options. The session
      // cookie defaults to Secure, which browsers don't reliably send over
      // http://localhost — so the session looked intermittently "lost" in dev.
      // Disable Secure in dev only; production (https) keeps Secure cookies.
      sessionConfiguration: {
        cookie: {
          secure: process.env.NODE_ENV === 'production',
        },
      },
    },
    signupEmailVerification: {
      clientId: '',
      clientSecret: '',
    },
    public: {
      // Deployment-specific values use matching NUXT_PUBLIC_* environment
      // variables at container startup. VITE_* remains the build-time fallback
      // for existing development and Vercel deployments.
      baseUrl: config.baseUrl,
      environment: config.environment,
      googleCloudStorageBucket: config.googleCloudStorageBucket,
      googleMapsApiKey: config.googleMapsApiKey,
      googleMapId: config.googleMapId,
      graphqlUrl: frontendGraphqlProxyUrl,
      logoutUrl: config.logoutUrl,
      openCageApiKey: config.openCageApiKey,
      openGraphApiKey: config.openGraphApiKey,
      serverName: config.serverName,
      serverDisplayName: config.serverDisplayName,
      // Instance branding (docs/source/support links and footer text). Nuxt
      // maps each nested key to a NUXT_PUBLIC_BRANDING_* variable, so a single
      // built image can be re-branded at container startup without a rebuild.
      // See docs/frontend-runtime-configuration.md.
      branding: brandingEnvDefaults,
      // When true, the values above win over the admin-editable ServerConfig
      // branding and the admin form renders read-only.
      brandingLocked: brandingLockedDefault,
      authProvider:
        process.env.NUXT_PUBLIC_AUTH_PROVIDER === 'local-dev'
          ? 'local-dev'
          : 'auth0',
      apollo: {
        clients: {
          default: {
            httpEndpoint: frontendGraphqlProxyUrl,
          },
        },
      },
      googleMapsApiKey: config?.googleMapsApiKey,
      enableAccented:
        process.env.NUXT_PUBLIC_ENABLE_ACCENTED === 'false' ? false : true,
    },
  },
  ssr: !isMockedE2E,
  // Emit client source maps for the E2E coverage build so browser V8 coverage
  // can be mapped back to original source (spike: E2E_COVERAGE=true).
  sourcemap: {
    client: process.env.E2E_COVERAGE === 'true',
  },
  typescript: {
    strict: false,
    shim: true,
    typeCheck: false,
  },
  vite: {
    plugins: [tailwindcss()],
    // Pre-bundle these deps at startup. Removing Vuetify changed the lockfile,
    // which forces Vite to re-optimize from scratch; without this list it
    // discovers some deps (notably ones pulled in by client-only plugins) only
    // at runtime and issues an HMR full-reload to swap them in. That reload can
    // interrupt an in-flight Playwright navigation and blank the page (the
    // create/edit E2E flows were failing for this reason). The list is exactly
    // what `nuxt dev` reports as "discovered new dependencies at runtime".
    optimizeDeps: {
      include: [
        '@floating-ui/vue',
        '@google/model-viewer',
        '@vue/apollo-composable',
        'accented',
        'luxon',
        'three',
      ],
    },
    resolve: {
      alias: {
        '@': path.resolve(__dirname),
        'fast-deep-equal': 'fast-deep-equal/es6/index.js',
      },
    },
    $client: {
      resolve: {
        alias: {
          // See stubs/postcss.ts: sanitize-html only needs postcss for style
          // attribute parsing, which the markdown renderer disables.
          postcss: path.resolve(__dirname, 'stubs/postcss.ts'),
        },
      },
    },
    define: {
      global: 'globalThis',
      // Keep Vue's production hydration warnings actionable. Without this,
      // production only reports that a mismatch occurred and omits the server
      // DOM node and expected client vnode needed to locate the source.
      __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: true,
    },
    // Allow connections from ngrok for mobile testing
    server: {
      allowedHosts: ['localhost', '69f8-98-168-53-208.ngrok-free.app'],
      watch: {
        // Exclude nested worktrees and generated artifacts from dev watchers
        // so `nuxt dev` does not exhaust file watchers in the main checkout.
        ignored: ignoredDevWatchPatterns,
      },
    },
    build: {
      // Keep production assets small enough to download, parse, and execute
      // quickly. These had been disabled globally, which shipped unminified
      // JavaScript and CSS to every visitor.
      minify: 'esbuild',
      cssMinify: 'esbuild',
      // terserOptions: {
      //   compress: {
      //     drop_console: process.env.NODE_ENV === 'production',
      //     drop_debugger: process.env.NODE_ENV === 'production'
      //   }
      // },
      rollupOptions: {
        output: {
          manualChunks(id) {
            for (const [chunkName, packageNames] of Object.entries(
              manualChunkGroups
            )) {
              if (
                packageNames.some((packageName) =>
                  id.includes(`/node_modules/${packageName}/`)
                )
              ) {
                return chunkName;
              }
            }
          },
        },
      },
    },
    optimizeDeps: {
      // Scan all pages so Vite discovers and pre-bundles every third-party
      // dependency upfront. Without this, the first client-side navigation to a
      // route whose components import a new (not yet pre-bundled) package causes
      // Vite to return 504 "Outdated Optimize Dep" responses, aborting the
      // router.push navigation (visible in CI as the createEditChannels test
      // timing out at the toHaveURL assertion).
      entries: ['./pages/**/*.vue'],
      include: [
        'vue',
        'vue-router',
        '@vue/apollo-composable',
        'luxon',
        'three',
        '@google/model-viewer',
      ],
    },
  },
});