Ctrl K
ComunidadGuías…

vite.config.mts

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

Archivo original ↗Preguntar a KiBot
import fs from "node:fs";
import path from "node:path";
import dotenv from "@dotenvx/dotenvx";
import react from "@vitejs/plugin-react";
import browserslistToEsbuild from "browserslist-to-esbuild";
import webpackStats from "rollup-plugin-webpack-stats";
import type { ConfigEnv, ServerOptions } from "vite";
import { defineConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";

export default ({ mode }: ConfigEnv) => {
  const env = loadEnvironment();

  let httpsConfig: ServerOptions["https"] | undefined;
  let host: string | undefined;

  // The Vite dev server listens on the port after the app server.
  const port = (parseInt(env("PORT") ?? "", 10) || 3000) + 1;

  if (env("NODE_ENV") === "development") {
    const url = env("URL");
    host = url ? new URL(url).hostname : undefined;

    try {
      httpsConfig = {
        key: fs.readFileSync("./server/config/certs/private.key"),
        cert: fs.readFileSync("./server/config/certs/public.cert"),
      };
    } catch {
      // oxlint-disable-next-line no-console
      console.warn("No local SSL certs found, HTTPS will not be available");
    }
  }

  return defineConfig({
    // The local .env file can set NODE_ENV=development, which would cause Vite
    // to bundle development builds of dependencies such as React. Derive the
    // bundled value from the Vite mode instead so `vite build` always bundles
    // production dependencies.
    define: {
      "process.env.NODE_ENV": JSON.stringify(
        mode === "development" ? "development" : "production"
      ),
    },
    root: "./",
    publicDir: "./server/static",
    base: (env("CDN_URL") ?? "") + "/static/",
    server: {
      port,
      strictPort: true,
      host: true,
      https: httpsConfig,
      allowedHosts: host ? [host] : undefined,
      cors: true,
      fs:
        env("NODE_ENV") === "development"
          ? {
              // Allow serving files from one level up to the project root
              allow: [".."],
            }
          : { strict: true },
    },
    plugins: [
      react(),
      // https://vite-pwa-org.netlify.app/
      VitePWA({
        injectRegister: "inline",
        registerType: "autoUpdate",
        workbox: {
          maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
          globPatterns: ["**/*.{css,ico,png,svg}"],
          navigateFallback: null,
          modifyURLPrefix: {
            "": `${env("CDN_URL") ?? ""}/static/`,
          },
          skipWaiting: true,
          clientsClaim: true,
          cleanupOutdatedCaches: true,
          runtimeCaching: [
            {
              // Chunks are content-hashed and immutable, so cache them on
              // first use rather than precaching the entire build.
              urlPattern: ({ url }) =>
                url.pathname.startsWith("/static/assets/") &&
                url.pathname.endsWith(".js"),
              handler: "CacheFirst",
              options: {
                cacheName: "js-cache",
                expiration: {
                  maxEntries: 200,
                  maxAgeSeconds: 2592000, // 30 days
                },
                cacheableResponse: {
                  statuses: [200],
                },
              },
            },
            {
              urlPattern: /api\/urls\.unfurl$/,
              handler: "CacheOnly",
              options: {
                cacheName: "unfurl-cache",
                expiration: {
                  maxEntries: 100,
                  maxAgeSeconds: 60 * 60,
                },
                cacheableResponse: {
                  statuses: [0, 200],
                },
              },
            },
            {
              // Limited to images, as media and PDFs are loaded with byte range
              // requests which Safari cannot play back when the response is
              // served by a service worker.
              urlPattern: ({ url, request }) =>
                url.pathname.endsWith("/api/files.get") &&
                request.destination === "image" &&
                !request.headers.has("range"),
              handler: "CacheFirst",
              options: {
                cacheName: "files-cache",
                expiration: {
                  maxEntries: 50,
                  maxAgeSeconds: 604800, // 7 days
                },
                cacheableResponse: {
                  statuses: [200],
                },
              },
            },
          ],
        },
        manifest: {
          name: "Outline",
          short_name: "Outline",
          theme_color: "#fff",
          background_color: "#fff",
          start_url: "/",
          scope: ".",
          display: "standalone",
          // For Chrome, you must provide at least a 192x192 pixel icon, and a 512x512 pixel icon.
          // If only those two icon sizes are provided, Chrome will automatically scale the icons
          // to fit the device. If you'd prefer to scale your own icons, and adjust them for
          // pixel-perfection, provide icons in increments of 48dp.
          icons: [
            {
              src: "/images/icon-192.png",
              sizes: "192x192",
              type: "image/png",
            },
            {
              src: "/images/icon-512.png",
              sizes: "512x512",
              type: "image/png",
            },
            {
              src: "/images/icon-maskable-192.png",
              sizes: "192x192",
              type: "image/png",
              purpose: "maskable",
            },
            {
              src: "/images/icon-maskable-512.png",
              sizes: "512x512",
              type: "image/png",
              purpose: "maskable",
            },
            {
              src: "/images/icon-maskable-1024.png",
              sizes: "1024x1024",
              type: "image/png",
              purpose: "maskable",
            },
            {
              src: "/images/icon-monochrome-512.png",
              sizes: "512x512",
              type: "image/png",
              purpose: "monochrome",
            },
            {
              src: "/images/icon-monochrome-1024.png",
              sizes: "1024x1024",
              type: "image/png",
              purpose: "monochrome",
            },
          ],
        },
      }),
      // Generate a stats.json file for webpack that will be consumed by RelativeCI
      webpackStats(),
    ],
    resolve: {
      alias: {
        "~": path.resolve(import.meta.dirname, "./app"),
        "@shared": path.resolve(import.meta.dirname, "./shared"),
      },
    },
    build: {
      outDir: "./build/app",
      manifest: true,
      sourcemap: process.env.CI ? false : "hidden",
      minify: "oxc",
      // Prevent asset inlining as it does not conform to CSP rules
      assetsInlineLimit: 0,
      target: browserslistToEsbuild(),
      reportCompressedSize: false,
      rollupOptions: {
        onwarn(warning, warn) {
          // Suppress noisy warnings about module-level directives, e.g. "use client"
          if (warning.code === "MODULE_LEVEL_DIRECTIVE") {
            return;
          }
          warn(warning);
        },
        input: {
          index: "./app/index.tsx",
        },
        output: {
          assetFileNames: "assets/[name].[hash][extname]",
          chunkFileNames: "assets/[name].[hash].js",
          entryFileNames: "assets/[name].[hash].js",
          codeSplitting: {
            groups: [
              // Shared utilities used across the app — higher priority
              // prevents them being absorbed into lazy vendor chunks
              {
                name: "vendor-shared",
                test: /node_modules[\\/]uuid|vite[\\/]preload-helper/,
                priority: 30,
              },
              {
                name: "vendor-react",
                test: /node_modules[\\/](react|react-dom|scheduler|react-router|react-router-dom|history)[\\/]/,
                priority: 20,
              },
              {
                name: "vendor-prosemirror",
                test: /node_modules[\\/](@benrbray[\\/])?prosemirror/,
                priority: 20,
              },
              {
                name: "vendor-collab",
                test: /node_modules[\\/](yjs|y-prosemirror|@hocuspocus|lib0)/,
                priority: 20,
              },
              {
                name: "vendor-framer-motion",
                test: /node_modules[\\/]framer-motion/,
                priority: 20,
              },
              {
                name: "vendor-styled",
                test: /node_modules[\\/](styled-components|stylis|@emotion[\\/](is-prop-valid|memoize))/,
                priority: 22,
              },
              {
                name: "vendor-katex",
                test: /node_modules[\\/]katex/,
                priority: 20,
              },
              {
                name: "vendor-emoji",
                test: /node_modules[\\/](@emoji-mart|emoji-mart)/,
                priority: 20,
              },
              {
                name: "vendor-es-toolkit",
                test: /node_modules[\\/]es-toolkit/,
                priority: 22,
              },
              {
                name: "vendor-date",
                test: /node_modules[\\/]date-fns/,
                priority: 20,
              },
              {
                name: "vendor-sentry",
                test: /node_modules[\\/]@sentry/,
                priority: 20,
              },
            ],
          },
        },
      },
    },
  });
};

/**
 * Loads the .env files with the same precedence as server/utils/environment.ts.
 * That file cannot be imported here, as Vite loads it as CommonJS.
 *
 * @returns a getter for a variable, where process.env takes precedence and an
 *   unset variable is read from the file named by its `_FILE` variable.
 */
function loadEnvironment(): (key: string) => string | undefined {
  const read = (file: string) => {
    const filePath = path.resolve(import.meta.dirname, file);
    return fs.existsSync(filePath)
      ? dotenv.parse(fs.readFileSync(filePath, "utf8"))
      : {};
  };

  const envDefault = read(".env");
  const nodeEnvs = [process.env.NODE_ENV, envDefault.NODE_ENV];
  const isDevelopment = nodeEnvs.includes("development");

  // Load environment specific variables, in reverse order of precedence.
  const environment = ["production", "development", "local", "test"]
    .filter(
      (env) => nodeEnvs.includes(env) || (env === "local" && isDevelopment)
    )
    .reduce<Record<string, string>>(
      (acc, env) => ({ ...acc, ...read(`.env.${env}`) }),
      {}
    );

  const merged: Record<string, string | undefined> = {
    ...envDefault,
    ...environment,
    ...process.env,
  };

  return (key) => {
    const value = merged[key];
    const filePath = merged[`${key}_FILE`];
    if (value !== undefined || !filePath) {
      return value;
    }

    try {
      return fs.readFileSync(filePath, "utf8").trim();
    } catch {
      // oxlint-disable-next-line no-console
      console.error(`Failed to read file for ${key}_FILE`);
      return undefined;
    }
  };
}