vite.config.mts
Rama main · réplica de lectura del repositorio original.
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;
}
};
}