UNPKG

rwsdk

Version:

Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime

202 lines (201 loc) 7.98 kB
import fs from "node:fs"; import path from "node:path"; import { normalizePath } from "vite"; import { ENV_RESOLVERS, maybeResolveEnvImport } from "./envResolvers.mjs"; const BARE_SPECIFIER_RE = /^(?:@[^/]+\/)?[^/]+/; function normalizePathSeparators(p) { return normalizePath(p.replace(/\\/g, "/")); } function getResolverForEnv(envName) { return envName in ENV_RESOLVERS ? envName : "client"; } function resolveExcludeRoot(entry, projectRootDir, envName) { projectRootDir = normalizePathSeparators(path.resolve(projectRootDir)); entry = normalizePathSeparators(entry); // Absolute filesystem path: keep it but resolve symlinks. if (path.isAbsolute(entry)) { try { return normalizePathSeparators(fs.realpathSync(entry)); } catch { return entry; } } // Relative path: resolve from the project root. if (entry.startsWith("./") || entry.startsWith("../")) { const resolved = normalizePathSeparators(path.resolve(projectRootDir, entry)); try { return normalizePathSeparators(fs.realpathSync(resolved)); } catch { return resolved; } } // Bare specifier (package, scoped package, or package subpath). const match = entry.match(BARE_SPECIFIER_RE); if (match) { const pkg = match[0]; const subpath = entry.slice(pkg.length); const pkgJsonPath = maybeResolveEnvImport({ id: `${pkg}/package.json`, envName, projectRootDir, }); if (!pkgJsonPath) { return undefined; } const pkgRoot = normalizePathSeparators(path.dirname(pkgJsonPath)); const resolvedRoot = subpath ? normalizePathSeparators(path.join(pkgRoot, subpath)) : pkgRoot; try { return normalizePathSeparators(fs.realpathSync(resolvedRoot)); } catch { return resolvedRoot; } } // Anything else is treated as root-relative. return normalizePathSeparators(path.join(projectRootDir, entry)); } /** * Collect `optimizeDeps.exclude` patterns from the root config and from every * environment config into a single, deduplicated list. */ export function getOptimizeDepsExcludePatterns(config) { const patterns = new Set(); for (const entry of config.optimizeDeps?.exclude ?? []) { if (entry) { patterns.add(entry); } } for (const env of Object.values(config.environments ?? {})) { for (const entry of env?.optimizeDeps?.exclude ?? []) { if (entry) { patterns.add(entry); } } } return [...patterns]; } /** * Collect `optimizeDeps.exclude` patterns grouped by Vite environment. * * Root-level excludes apply to every environment. Per-environment excludes * apply only to that environment. When no environments are configured (e.g. * Vite 7), the known RedwoodSDK environments (`client`, `ssr`, `worker`) are * populated with the root-level patterns. */ export function getOptimizeDepsExcludePatternsByEnv(config) { const rootPatterns = config.optimizeDeps?.exclude ?? []; const environments = config.environments ?? {}; const patternsByEnv = {}; for (const envName of Object.keys(environments)) { patternsByEnv[envName] = [...rootPatterns]; } // Fallback for non-environmental configs. if (Object.keys(patternsByEnv).length === 0) { for (const envName of Object.keys(ENV_RESOLVERS)) { patternsByEnv[envName] = [...rootPatterns]; } } for (const [envName, env] of Object.entries(environments)) { for (const entry of env?.optimizeDeps?.exclude ?? []) { if (entry) { patternsByEnv[envName].push(entry); } } } return patternsByEnv; } function resolveOptimizeDepsExcludesForEnv(excludes, projectRootDir, envName) { const roots = new Set(); for (const entry of excludes) { if (!entry) { continue; } const root = resolveExcludeRoot(entry, projectRootDir, envName); if (root) { roots.add(root); continue; } // If the environment resolver couldn't locate the package, fall back to a // node_modules path so the exclusion still has a chance to match. const match = entry.match(BARE_SPECIFIER_RE); if (match) { roots.add(normalizePathSeparators(path.join(projectRootDir, "node_modules", entry))); } } return [...roots]; } /** * Resolve per-environment `optimizeDeps.exclude` patterns into absolute * filesystem roots. Each environment is resolved with the environment-aware * resolver that matches its execution context. */ export function resolveOptimizeDepsExcludesByEnv(patternsByEnv, projectRootDir) { const rootsByEnv = {}; for (const [envName, patterns] of Object.entries(patternsByEnv)) { rootsByEnv[envName] = resolveOptimizeDepsExcludesForEnv(patterns, projectRootDir, getResolverForEnv(envName)); } return rootsByEnv; } /** * Resolve entries from Vite's `optimizeDeps.exclude` into absolute filesystem * roots that can be matched against discovered directive files. * * Supports: * - Bare package names (`my-ui-lib`) * - Scoped packages (`@scope/pkg`) * - Package subpaths (`my-ui-lib/components`) * - Relative paths (`./packages/my-ui-lib`) * - Absolute paths * * Symlinked packages (e.g. `file:./packages/my-ui-lib`) are resolved to their * real location on disk, so source files are matched even though the import * specifier goes through `node_modules`. * * Each pattern is resolved through RedwoodSDK's environment-aware resolvers so * that packages with environment-specific exports are located correctly for the * client, SSR, and worker environments. The resolved roots from all * environments are unioned together. */ export function resolveOptimizeDepsExcludes(excludes, projectRootDir) { const envNames = Object.keys(ENV_RESOLVERS); const roots = new Set(); for (const envName of envNames) { for (const root of resolveOptimizeDepsExcludesForEnv(excludes, projectRootDir, envName)) { roots.add(root); } } return [...roots]; } export function isExcludedFromOptimization(file, excludedRoots, projectRootDir) { const normalizedFile = normalizePathSeparators(file); const candidates = new Set(); candidates.add(normalizedFile); if (projectRootDir) { const root = normalizePathSeparators(path.resolve(projectRootDir)); // If the file isn't already an absolute path inside the project root, also // try resolving it from the project root. This covers both relative paths // and Vite-style project-relative paths such as `/node_modules/foo/index.js`, // which is how files inside the project root are represented after // `normalizeModulePath`. We keep the original candidate too so external // absolute paths (e.g. symlinked monorepo packages) still match their own // excluded roots even if resolving from the project root produces a bogus // path. if (normalizedFile !== root && !normalizedFile.startsWith(root + "/")) { const relativePart = normalizedFile.startsWith("/") ? normalizedFile.slice(1) : normalizedFile; candidates.add(normalizePathSeparators(path.resolve(root, relativePart))); } } return excludedRoots.some((excludedRoot) => { const normalizedExcludedRoot = normalizePathSeparators(excludedRoot); const prefix = normalizedExcludedRoot.endsWith("/") ? normalizedExcludedRoot : normalizedExcludedRoot + "/"; return Array.from(candidates).some((candidate) => candidate === normalizedExcludedRoot || candidate.startsWith(prefix)); }); }