UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

426 lines (425 loc) • 15.6 kB
import { createHash } from "node:crypto"; import nodeFs from "node:fs"; import { fileURLToPath } from "node:url"; import * as vite from "vite"; import { crawlFrameworkPkgs } from "vitefu"; import { vitePluginActions } from "../actions/vite-plugin-actions.js"; import { getAssetsPrefix } from "../assets/utils/getAssetsPrefix.js"; import astroAssetsPlugin from "../assets/vite-plugin-assets.js"; import astroContainer from "../container/vite-plugin-container.js"; import { astroContentAssetPropagationPlugin, astroContentImportPlugin, astroContentVirtualModPlugin } from "../content/index.js"; import { createEnvLoader } from "../env/env-loader.js"; import { validateEnvPrefixAgainstSchema } from "../env/validators.js"; import { astroEnv } from "../env/vite-plugin-env.js"; import { importMetaEnv } from "../env/vite-plugin-import-meta-env.js"; import astroInternationalization from "../i18n/vite-plugin-i18n.js"; import { serializedManifestPlugin } from "../manifest/serialized.js"; import astroVirtualManifestPlugin from "../manifest/virtual-module.js"; import astroPrefetch from "../prefetch/vite-plugin-prefetch.js"; import astroDevToolbar from "../toolbar/vite-plugin-dev-toolbar.js"; import astroTransitions from "../transitions/vite-plugin-transitions.js"; import { vitePluginAdapterConfig } from "../vite-plugin-adapter-config/index.js"; import { vitePluginApp } from "../vite-plugin-app/index.js"; import { vitePluginFetchable } from "./fetch/vite-plugin.js"; import astroVitePlugin from "../vite-plugin-astro/index.js"; import { vitePluginAstroServer } from "../vite-plugin-astro-server/index.js"; import configAliasVitePlugin from "../vite-plugin-config-alias/index.js"; import { astroDevCssContentCachePlugin, astroDevCssPlugin } from "../vite-plugin-css/index.js"; import vitePluginFileURL from "../vite-plugin-fileurl/index.js"; import astroHeadPlugin from "../vite-plugin-head/index.js"; import astroHmrReloadPlugin from "../vite-plugin-hmr-reload/index.js"; import htmlVitePlugin from "../vite-plugin-html/index.js"; import astroIntegrationsContainerPlugin from "../vite-plugin-integrations-container/index.js"; import astroLoadFallbackPlugin from "../vite-plugin-load-fallback/index.js"; import markdownVitePlugin from "../vite-plugin-markdown/index.js"; import { pluginPage, pluginPages } from "../vite-plugin-pages/index.js"; import vitePluginRenderers from "../vite-plugin-renderers/index.js"; import astroPluginRoutes from "../vite-plugin-routes/index.js"; import vitePluginStaticPaths from "../vite-plugin-static-paths/index.js"; import astroScriptsPlugin from "../vite-plugin-scripts/index.js"; import astroScriptsPageSSRPlugin from "../vite-plugin-scripts/page-ssr.js"; import { createViteLogger } from "./logger/vite.js"; import { vitePluginMiddleware } from "./middleware/vite-plugin.js"; import { joinPaths } from "./path.js"; import { ServerIslandsState } from "./server-islands/shared-state.js"; import { vitePluginServerIslands } from "./server-islands/vite-plugin-server-islands.js"; import { vitePluginCacheProvider } from "./cache/vite-plugin.js"; import { vitePluginSessionDriver, vitePluginSessionProvider } from "./session/vite-plugin.js"; import { vitePluginLogger } from "./logger/vite-plugin.js"; import { isObject } from "./util-runtime.js"; import { vitePluginEnvironment } from "../vite-plugin-environment/index.js"; import { ASTRO_VITE_ENVIRONMENT_NAMES } from "./constants.js"; import { vitePluginChromedevtools } from "../vite-plugin-chromedevtools/index.js"; import { vitePluginDevStatus } from "../vite-plugin-dev-status/index.js"; import { vitePluginAstroServerClient } from "../vite-plugin-overlay/index.js"; // In-process cache for crawlFrameworkPkgs results. The crawl walks the entire // node_modules tree reading package.json files, which is expensive and produces // the same result for a given (root, isBuild) pair within a single process lifetime. const _crawlCache = new Map(); function cloneCrawlResult(result) { return { optimizeDeps: { include: [...result.optimizeDeps.include], exclude: [...result.optimizeDeps.exclude] }, ssr: { noExternal: [...result.ssr.noExternal], external: [...result.ssr.external] } }; } /** * Clear the crawlFrameworkPkgs cache. Call this when node_modules may have * changed (e.g. after a dev server restart triggered by config/lockfile change). */ export function clearCrawlCache() { _crawlCache.clear(); } /** Return a base vite config as a common starting point for all Vite commands. */ export async function createVite(commandConfig, { settings, logger, mode, command, fs = nodeFs, sync, routesList }) { const root = fileURLToPath(settings.config.root); const isBuild = command === "build"; const crawlCacheKey = `${root}:${isBuild}`; let astroPkgsConfig = _crawlCache.get(crawlCacheKey); if (!astroPkgsConfig) { astroPkgsConfig = await crawlFrameworkPkgs({ root, isBuild, viteUserConfig: settings.config.vite, isFrameworkPkgByJson(pkgJson) { // Certain packages will trigger the checks below, but need to be external. A common example are SSR adapters // for node-based platforms, as we need to control the order of the import paths to make sure polyfills are applied in time. if (pkgJson?.astro?.external === true) { return false; } return pkgJson.peerDependencies?.astro || pkgJson.dependencies?.astro || pkgJson.keywords?.includes("astro") || pkgJson.keywords?.includes("astro-component") || /^(?:@[^/]+\/)?astro-/.test(pkgJson.name); }, isFrameworkPkgByName(pkgName) { const isNotAstroPkg = isCommonNotAstro(pkgName); if (isNotAstroPkg) { return false; } else { return undefined; } } }); _crawlCache.set(crawlCacheKey, astroPkgsConfig); } // Return a clone so consumers can't mutate the cached result astroPkgsConfig = cloneCrawlResult(astroPkgsConfig); const envLoader = createEnvLoader({ mode, config: settings.config }); const serverIslandsState = new ServerIslandsState(); // Shared cache of CSS content by module ID. Populated by the dev-css content cache // plugin's transform hook and consumed by the content asset propagation plugin to avoid // re-processing CSS modules with `?inline` (which produces different // scoped-name hashes with Lightning CSS). const cssContentCache = new Map(); // Validate that envPrefix doesn't conflict with secret env schema variables validateEnvPrefixAgainstSchema(settings.config); // Start with the Vite configuration that Astro core needs const commonConfig = { // Tell Vite not to combine config from vite.config.js with our provided inline config configFile: false, mode, cacheDir: fileURLToPath(new URL("./node_modules/.vite/", settings.config.root)), clearScreen: false, customLogger: createViteLogger(logger, settings.config.vite.logLevel), appType: "custom", plugins: [ ( // Raise the watcher's maxListeners limit before any other plugin's // configureServer hook can add listeners. Astro registers 12+ change // listeners across its built-in Vite plugins, easily exceeding // Node's default limit of 10. { name: "astro:watcher-max-listeners", configureServer(server) { const current = server.watcher.getMaxListeners(); if (current !== 0 && current < 50) { server.watcher.setMaxListeners(50); } } }), serializedManifestPlugin({ settings, command, sync }), vitePluginRenderers({ settings, routesList, serverIslandsState, command: command === "dev" ? "serve" : "build" }), vitePluginStaticPaths(), await astroPluginRoutes({ routesList, settings, logger, fsMod: fs, command }), astroVirtualManifestPlugin({ settings }), vitePluginEnvironment({ settings, astroPkgsConfig, command }), pluginPage({ routesList }), pluginPages({ routesList }), configAliasVitePlugin({ settings }), astroLoadFallbackPlugin({ fs, root: settings.config.root }), astroVitePlugin({ settings, logger }), astroScriptsPlugin({ settings }), vitePluginApp(), vitePluginFetchable({ settings }), command === "dev" && vitePluginAstroServer({ settings, logger }), command === "dev" && vitePluginAstroServerClient(), astroDevCssPlugin({ routesList, command, cssContentCache }), importMetaEnv({ envLoader }), astroEnv({ settings, sync, envLoader }), vitePluginAdapterConfig(settings), markdownVitePlugin({ settings, logger }), htmlVitePlugin(), astroIntegrationsContainerPlugin({ settings, logger }), astroScriptsPageSSRPlugin({ settings }), astroHeadPlugin(), astroContentVirtualModPlugin({ fs, settings }), astroContentImportPlugin({ fs, settings, logger }), astroContentAssetPropagationPlugin({ settings, cssContentCache }), vitePluginMiddleware({ settings }), astroAssetsPlugin({ fs, settings, sync, logger }), astroPrefetch({ settings }), astroTransitions({ settings }), astroDevToolbar({ settings, logger }), vitePluginFileURL(), astroInternationalization({ settings }), vitePluginActions({ fs, settings }), vitePluginServerIslands({ settings, logger, serverIslandsState }), vitePluginSessionDriver({ settings }), vitePluginSessionProvider({ settings }), vitePluginCacheProvider({ settings }), vitePluginLogger({ settings }), astroContainer(), astroHmrReloadPlugin(), vitePluginChromedevtools({ settings }), command === "dev" && vitePluginDevStatus() ], publicDir: fileURLToPath(settings.config.publicDir), root: fileURLToPath(settings.config.root), envPrefix: settings.config.vite?.envPrefix ?? "PUBLIC_", define: { "import.meta.env.SITE": stringifyForDefine(settings.config.site), "import.meta.env.BASE_URL": stringifyForDefine(settings.config.base), "import.meta.env.ASSETS_PREFIX": stringifyForDefine(settings.config.build.assetsPrefix) }, server: { hmr: process.env.NODE_ENV === "test" || process.env.NODE_ENV === "production" ? false : undefined, watch: { // Prevent watching during the build to speed it up ignored: command === "build" ? ["**"] : undefined } }, resolve: { // Vite's native tsconfig path resolution; see configAliasVitePlugin for the deprecated fallback. tsconfigPaths: true, alias: [ { // This is needed for Deno compatibility, as the non-browser version // of this module depends on Node `crypto` find: "randombytes", replacement: "randombytes/browser" }, { // Typings are imported from 'astro' (e.g. import { Type } from 'astro') find: /^astro$/, replacement: fileURLToPath(new URL("../types/public/index.js", import.meta.url)) }, { find: "astro:middleware", replacement: "astro/virtual-modules/middleware.js" }, ( // TODO: remove in Astro 8 { find: "astro:schema", replacement: "astro/zod" }), { find: "astro:components", replacement: "astro/components" } ] }, build: { assetsDir: settings.config.build.assets }, css: !isBuild ? { modules: { // Vite's default postcss-modules scoped-name generator hashes the CSS // *content*, so every declaration edit changes class names. In dev this // breaks HMR for server-rendered HTML because the DOM still references // the previous names. Use a path-based hash instead so selectors stay // stable and Vite's CSS HMR can swap rules without a full page reload. generateScopedName(name, filename) { const hash = createHash("sha256").update(filename + "\0" + name).digest("base64url").substring(0, 5); return `_${name}_${hash}`; } } } : undefined, environments: { [ASTRO_VITE_ENVIRONMENT_NAMES.astro]: { // This is all that's needed to create a new RunnableDevEnvironment dev: {} } } }; // If the user provides a custom assets prefix, make sure assets handled by Vite // are prefixed with it too. This uses one of its experimental features, but it // has been stable for a long time now. const assetsPrefix = settings.config.build.assetsPrefix; if (assetsPrefix) { commonConfig.experimental = { renderBuiltUrl(filename, { type, hostType }) { if (type === "asset") { return joinPaths(getAssetsPrefix(`.${hostType}`, assetsPrefix), filename); } } }; } // Merge configs: we merge vite configuration objects together in the following order, // where future values will override previous values. // 1. common vite config // 2. user-provided vite config, via AstroConfig // 3. integration-provided vite config, via the `config:setup` hook // 4. command vite config, passed as the argument to this function let result = commonConfig; // PR #6238 Calls user integration `astro:config:setup` hooks when running `astro sync`. // Without proper filtering, user integrations may run twice unexpectedly: // - with `command` set to `build/dev` (src/core/build/index.ts L72) // - and again in the `sync` module to generate `Content Collections` (src/core/sync/index.ts L36) // We need to check if the command is `build` or `dev` before merging the user-provided vite config. // We also need to filter out the plugins that are not meant to be applied to the current command: // - If the command is `build`, we filter out the plugins that are meant to be applied for `serve`. // - If the command is `dev`, we filter out the plugins that are meant to be applied for `build`. if (command && settings.config.vite?.plugins) { let { plugins, ...rest } = settings.config.vite; const applyToFilter = command === "build" ? "serve" : "build"; const applyArgs = [{ ...settings.config.vite, mode }, { command: command === "dev" ? "serve" : command, mode }]; // @ts-expect-error ignore TS2589: Type instantiation is excessively deep and possibly infinite. plugins = plugins.flat(Number.POSITIVE_INFINITY).filter((p) => { if (!p || p?.apply === applyToFilter) { return false; } if (typeof p.apply === "function") { return p.apply(applyArgs[0], applyArgs[1]); } return true; }); result = vite.mergeConfig(result, { ...rest, plugins }); } else { result = vite.mergeConfig(result, settings.config.vite || {}); } result = vite.mergeConfig(result, commandConfig); if (command === "dev") { // Appended last so it sees CSS after integration plugins have transformed it. result = vite.mergeConfig(result, { plugins: [astroDevCssContentCachePlugin({ cssContentCache })] }); } return result; } const COMMON_DEPENDENCIES_NOT_ASTRO = [ "autoprefixer", "react", "react-dom", "preact", "preact-render-to-string", "vue", "svelte", "solid-js", "lit", "cookie", "dotenv", "esbuild", "eslint", "jest", "postcss", "prettier", "astro", "tslib", "typescript", "vite" ]; const COMMON_PREFIXES_NOT_ASTRO = [ "@webcomponents/", "@fontsource/", "@postcss-plugins/", "@rolldown/", "@rollup/", "@astrojs/renderer-", "@types/", "@typescript-eslint/", "eslint-", "jest-", "postcss-plugin-", "prettier-plugin-", "remark-", "rehype-", "rolldown-plugin-", "rollup-plugin-", "vite-plugin-" ]; function isCommonNotAstro(dep) { return COMMON_DEPENDENCIES_NOT_ASTRO.includes(dep) || COMMON_PREFIXES_NOT_ASTRO.some((prefix) => prefix.startsWith("@") ? dep.startsWith(prefix) : dep.substring(dep.lastIndexOf("/") + 1).startsWith(prefix)); } function stringifyForDefine(value) { return typeof value === "string" || isObject(value) ? JSON.stringify(value) : "undefined"; }