diff --git a/packages/astro/src/config/entrypoint.ts b/packages/astro/src/config/entrypoint.ts index da57c538b4df..8c7efbc4c0c4 100644 --- a/packages/astro/src/config/entrypoint.ts +++ b/packages/astro/src/config/entrypoint.ts @@ -1,6 +1,7 @@ // IMPORTANT: this file is the entrypoint for "astro/config". Keep it as light as possible! import type { SharpImageServiceConfig } from '../assets/services/sharp.js'; + import type { ImageServiceConfig } from '../types/public/index.js'; export { defineAstroFontProvider, fontProviders } from '../assets/fonts/providers/index.js'; diff --git a/packages/astro/src/container/index.ts b/packages/astro/src/container/index.ts index 1879b1732f66..aa59b7671312 100644 --- a/packages/astro/src/container/index.ts +++ b/packages/astro/src/container/index.ts @@ -135,14 +135,15 @@ function createManifest( }; } + const root = new URL(import.meta.url); return { - hrefRoot: import.meta.url, - srcDir: manifest?.srcDir ?? ASTRO_CONFIG_DEFAULTS.srcDir, - buildClientDir: manifest?.buildClientDir ?? ASTRO_CONFIG_DEFAULTS.build.client, - buildServerDir: manifest?.buildServerDir ?? ASTRO_CONFIG_DEFAULTS.build.server, - publicDir: manifest?.publicDir ?? ASTRO_CONFIG_DEFAULTS.publicDir, - outDir: manifest?.outDir ?? ASTRO_CONFIG_DEFAULTS.outDir, - cacheDir: manifest?.cacheDir ?? ASTRO_CONFIG_DEFAULTS.cacheDir, + rootDir: root, + srcDir: manifest?.srcDir ?? new URL(ASTRO_CONFIG_DEFAULTS.srcDir, root), + buildClientDir: manifest?.buildClientDir ?? new URL(ASTRO_CONFIG_DEFAULTS.build.client, root), + buildServerDir: manifest?.buildServerDir ?? new URL(ASTRO_CONFIG_DEFAULTS.build.server, root), + publicDir: manifest?.publicDir ?? new URL(ASTRO_CONFIG_DEFAULTS.publicDir, root), + outDir: manifest?.outDir ?? new URL(ASTRO_CONFIG_DEFAULTS.outDir, root), + cacheDir: manifest?.cacheDir ?? new URL(ASTRO_CONFIG_DEFAULTS.cacheDir, root), trailingSlash: manifest?.trailingSlash ?? ASTRO_CONFIG_DEFAULTS.trailingSlash, buildFormat: manifest?.buildFormat ?? ASTRO_CONFIG_DEFAULTS.build.format, compressHTML: manifest?.compressHTML ?? ASTRO_CONFIG_DEFAULTS.compressHTML, diff --git a/packages/astro/src/content/types-generator.ts b/packages/astro/src/content/types-generator.ts index 7d2ccc17f09b..793ade64ff6d 100644 --- a/packages/astro/src/content/types-generator.ts +++ b/packages/astro/src/content/types-generator.ts @@ -5,6 +5,7 @@ import { bold, cyan } from 'kleur/colors'; import { glob } from 'tinyglobby'; import { type DevEnvironment, + isRunnableDevEnvironment, normalizePath, type RunnableDevEnvironment, type ViteDevServer, @@ -347,6 +348,9 @@ export async function createContentTypesGenerator({ logger, settings, }); + if (!isRunnableDevEnvironment(viteServer.environments.ssr)) { + return; + } invalidateVirtualMod(viteServer.environments.ssr); } } diff --git a/packages/astro/src/core/app/base.ts b/packages/astro/src/core/app/base.ts index 51616c80028a..f33b2ee78f6b 100644 --- a/packages/astro/src/core/app/base.ts +++ b/packages/astro/src/core/app/base.ts @@ -186,7 +186,6 @@ export abstract class BaseApp

{ pathname = prependForwardSlash(this.removeBase(url.pathname)); } let routeData = matchRoute(decodeURI(pathname), this.manifestData); - if (!routeData) return undefined; if (allowPrerenderedRoutes) { return routeData; diff --git a/packages/astro/src/core/app/common.ts b/packages/astro/src/core/app/common.ts index f07b6a8b5ef3..b88f471c2150 100644 --- a/packages/astro/src/core/app/common.ts +++ b/packages/astro/src/core/app/common.ts @@ -45,6 +45,13 @@ export function deserializeManifest( return { onRequest: NOOP_MIDDLEWARE_FN }; }, ...serializedManifest, + rootDir: new URL(serializedManifest.rootDir), + srcDir: new URL(serializedManifest.srcDir), + publicDir: new URL(serializedManifest.publicDir), + outDir: new URL(serializedManifest.outDir), + cacheDir: new URL(serializedManifest.cacheDir), + buildClientDir: new URL(serializedManifest.buildClientDir), + buildServerDir: new URL(serializedManifest.buildServerDir), assets, componentMetadata, inlinedScripts, diff --git a/packages/astro/src/core/app/dev/app.ts b/packages/astro/src/core/app/dev/app.ts index 91b8a2f9e5c3..28266ad5cbcc 100644 --- a/packages/astro/src/core/app/dev/app.ts +++ b/packages/astro/src/core/app/dev/app.ts @@ -1,383 +1,40 @@ -import type http from 'node:http'; -import { prependForwardSlash, removeTrailingForwardSlash } from '@astrojs/internal-helpers/path'; -import { loadActions } from '../../../actions/loadActions.js'; -import { getSortedPreloadedMatches } from '../../../prerender/routing.js'; -import type { AstroSettings, RoutesList } from '../../../types/astro.js'; +import type { RoutesList } from '../../../types/astro.js'; import type { RouteData } from '../../../types/public/index.js'; -import type { DevServerController } from '../../../vite-plugin-astro-server/controller.js'; -import { recordServerError } from '../../../vite-plugin-astro-server/error.js'; -import { runWithErrorHandling } from '../../../vite-plugin-astro-server/index.js'; -import { - handle500Response, - writeSSRResult, - writeWebResponse, -} from '../../../vite-plugin-astro-server/response.js'; -import { shouldAppendForwardSlash } from '../../build/util.js'; -import { - clientLocalsSymbol, - DEFAULT_404_COMPONENT, - NOOP_MIDDLEWARE_HEADER, - REROUTE_DIRECTIVE_HEADER, - REWRITE_DIRECTIVE_HEADER_KEY, -} from '../../constants.js'; -import { - MiddlewareNoDataOrNextCalled, - MiddlewareNotAResponse, - NoMatchingStaticPathFound, -} from '../../errors/errors-data.js'; +import { MiddlewareNoDataOrNextCalled, MiddlewareNotAResponse } from '../../errors/errors-data.js'; import { type AstroError, isAstroError } from '../../errors/index.js'; import type { Logger } from '../../logger/core.js'; -import { req } from '../../messages.js'; -import { loadMiddleware } from '../../middleware/loadMiddleware.js'; -import type { ModuleLoader } from '../../module-loader/index.js'; -import { routeIsRedirect } from '../../redirects/index.js'; -import { getProps } from '../../render/index.js'; import type { CreateRenderContext, RenderContext } from '../../render-context.js'; -import { createRequest } from '../../request.js'; -import { redirectTemplate } from '../../routing/3xx.js'; -import { matchAllRoutes } from '../../routing/index.js'; -import { isRoute404, isRoute500 } from '../../routing/match.js'; -import { PERSIST_SYMBOL } from '../../session.js'; +import { isRoute500 } from '../../routing/match.js'; import { BaseApp, type RenderErrorOptions } from '../base.js'; import type { SSRManifest } from '../types.js'; import { DevPipeline } from './pipeline.js'; export class DevApp extends BaseApp { - settings: AstroSettings; logger: Logger; - loader: ModuleLoader; - manifestData: RoutesList; currentRenderContext: RenderContext | undefined = undefined; - constructor( - manifest: SSRManifest, - streaming = true, - settings: AstroSettings, - logger: Logger, - loader: ModuleLoader, - manifestData: RoutesList, - ) { - super(manifest, streaming, settings, logger, loader, manifestData); - this.settings = settings; + constructor(manifest: SSRManifest, streaming = true, logger: Logger, routesList: RoutesList) { + super(manifest, streaming, logger); this.logger = logger; - this.loader = loader; - this.manifestData = manifestData; + this.manifestData = routesList; } - static async create( - manifest: SSRManifest, - routesList: RoutesList, - settings: AstroSettings, - logger: Logger, - loader: ModuleLoader, - ): Promise { - return new DevApp(manifest, true, settings, logger, loader, routesList); - } - - createPipeline( - _streaming: boolean, - manifest: SSRManifest, - settings: AstroSettings, - logger: Logger, - loader: ModuleLoader, - manifestData: RoutesList, - ): DevPipeline { - return DevPipeline.create(manifestData, { - loader, + createPipeline(streaming: boolean, manifest: SSRManifest, logger: Logger): DevPipeline { + return DevPipeline.create({ logger, manifest, - settings, + streaming, }); } + match(request: Request): RouteData | undefined { + return super.match(request, true); + } + async createRenderContext(payload: CreateRenderContext): Promise { this.currentRenderContext = await super.createRenderContext(payload); return this.currentRenderContext; } - public clearRouteCache() { - this.pipeline.clearRouteCache(); - } - - public async handleRequest({ - controller, - incomingRequest, - incomingResponse, - isHttps, - }: HandleRequest): Promise { - const { config } = this.pipeline; - const origin = `${isHttps ? 'https' : 'http'}://${ - incomingRequest.headers[':authority'] ?? incomingRequest.headers.host - }`; - - const url = new URL(origin + incomingRequest.url); - let pathname: string; - if (config.trailingSlash === 'never' && !incomingRequest.url) { - pathname = ''; - } else { - // We already have a middleware that checks if there's an incoming URL that has invalid URI, so it's safe - // to not handle the error: packages/astro/src/vite-plugin-astro-server/base.ts - pathname = decodeURI(url.pathname); - } - - // Add config.base back to url before passing it to SSR - url.pathname = removeTrailingForwardSlash(config.base) + url.pathname; - if ( - url.pathname.endsWith('/') && - !shouldAppendForwardSlash(config.trailingSlash, config.build.format) - ) { - url.pathname = url.pathname.slice(0, -1); - } - - let body: BodyInit | undefined = undefined; - if (!(incomingRequest.method === 'GET' || incomingRequest.method === 'HEAD')) { - let bytes: Uint8Array[] = []; - await new Promise((resolve) => { - incomingRequest.on('data', (part) => { - bytes.push(part); - }); - incomingRequest.on('end', resolve); - }); - body = Buffer.concat(bytes); - } - - const self = this; - await runWithErrorHandling({ - controller, - pathname, - async run() { - const matchedRoute = await matchRoute(pathname, self.manifestData, self.pipeline); - const resolvedPathname = matchedRoute?.resolvedPathname ?? pathname; - return await self.handleRoute({ - matchedRoute, - url, - pathname: resolvedPathname, - body, - incomingRequest: incomingRequest, - incomingResponse: incomingResponse, - }); - }, - onError(_err) { - const { error, errorWithMetadata } = recordServerError( - self.loader, - config, - self.logger, - _err, - ); - handle500Response(self.loader, incomingResponse, errorWithMetadata); - return error; - }, - }); - } - - async handleRoute({ - matchedRoute, - incomingRequest, - incomingResponse, - body, - url, - pathname, - }: HandleRoute): Promise { - const timeStart = performance.now(); - const { config, loader, logger } = this.pipeline; - - if (!matchedRoute) { - // This should never happen, because ensure404Route will add a 404 route if none exists. - throw new Error('No route matched, and default 404 route was not found.'); - } - - let request: Request; - let renderContext: RenderContext; - let route: RouteData = matchedRoute.route; - const componentInstance = await this.pipeline.getComponentByRoute(route); - const actions = await loadActions(loader); - this.pipeline.setActions(actions); - const middleware = (await loadMiddleware(loader)).onRequest; - // This is required for adapters to set locals in dev mode. They use a dev server middleware to inject locals to the `http.IncomingRequest` object. - const locals = Reflect.get(incomingRequest, clientLocalsSymbol); - - // Allows adapters to pass in locals in dev mode. - request = createRequest({ - url, - headers: incomingRequest.headers, - method: incomingRequest.method, - body, - logger, - isPrerendered: route.prerender, - routePattern: route.component, - }); - - // Set user specified headers to response object. - for (const [name, value] of Object.entries(config.server.headers ?? {})) { - if (value) incomingResponse.setHeader(name, value); - } - - renderContext = await this.createRenderContext({ - locals, - pipeline: this.pipeline, - pathname, - middleware: isDefaultPrerendered404(matchedRoute.route) ? undefined : middleware, - request, - routeData: route, - clientAddress: incomingRequest.socket.remoteAddress, - actions, - shouldInjectCspMetaTags: false, - }); - - let response; - let statusCode = 200; - let isReroute = false; - let isRewrite = false; - - try { - response = await renderContext.render(componentInstance); - isReroute = response.headers.has(REROUTE_DIRECTIVE_HEADER); - isRewrite = response.headers.has(REWRITE_DIRECTIVE_HEADER_KEY); - const statusCodedMatched = getStatusByMatchedRoute(route); - statusCode = isRewrite - ? // Ignore `matchedRoute` status for rewrites - response.status - : // Our internal noop middleware sets a particular header. If the header isn't present, it means that the user have - // their own middleware, so we need to return what the user returns. - !response.headers.has(NOOP_MIDDLEWARE_HEADER) && !isReroute - ? response.status - : (statusCodedMatched ?? response.status); - } catch (err: any) { - response = await this.renderError(request, { - skipMiddleware: false, - locals, - status: 500, - prerenderedErrorPageFetch: fetch, - clientAddress: incomingRequest.socket.remoteAddress, - error: err, - }); - statusCode = 500; - } finally { - this.currentRenderContext?.session?.[PERSIST_SYMBOL](); - } - - if (isLoggedRequest(pathname)) { - const timeEnd = performance.now(); - logger.info( - null, - req({ - url: pathname, - method: incomingRequest.method, - statusCode, - isRewrite, - reqTime: timeEnd - timeStart, - }), - ); - } - - if ( - statusCode === 404 && - // If the body isn't null, that means the user sets the 404 status - // but uses the current route to handle the 404 - response.body === null && - response.headers.get(REROUTE_DIRECTIVE_HEADER) !== 'no' - ) { - const fourOhFourRoute = await matchRoute('/404', this.manifestData, this.pipeline); - if (fourOhFourRoute) { - renderContext = await this.createRenderContext({ - locals, - pipeline: this.pipeline, - pathname, - middleware: isDefaultPrerendered404(fourOhFourRoute.route) ? undefined : middleware, - request, - routeData: fourOhFourRoute.route, - clientAddress: incomingRequest.socket.remoteAddress, - status: 404, - shouldInjectCspMetaTags: false, - }); - const component = await this.pipeline.preload( - fourOhFourRoute.route, - fourOhFourRoute.filePath, - ); - response = await renderContext.render(component); - } - } - - // We remove the internally-used header before we send the response to the user agent. - if (isReroute) { - response.headers.delete(REROUTE_DIRECTIVE_HEADER); - } - if (isRewrite) { - response.headers.delete(REROUTE_DIRECTIVE_HEADER); - } - - if (route.type === 'endpoint') { - await writeWebResponse(incomingResponse, response); - return; - } - - // This check is important in case of rewrites. - // A route can start with a 404 code, then the rewrite kicks in and can return a 200 status code - if (isRewrite) { - await writeSSRResult(request, response, incomingResponse); - return; - } - - // We are in a recursion, and it's possible that this function is called itself with a status code - // By default, the status code passed via parameters is computed by the matched route. - // - // By default, we should give priority to the status code passed, although it's possible that - // the `Response` emitted by the user is a redirect. If so, then return the returned response. - if (response.status < 400 && response.status >= 300) { - if ( - response.status >= 300 && - response.status < 400 && - routeIsRedirect(route) && - !config.build.redirects && - this.pipeline.settings.buildOutput === 'static' - ) { - // If we're here, it means that the calling static redirect that was configured by the user - // We try to replicate the same behaviour that we provide during a static build - const location = response.headers.get('location')!; - response = new Response( - redirectTemplate({ - status: response.status, - absoluteLocation: location, - relativeLocation: location, - from: pathname, - }), - { - status: 200, - headers: { - ...response.headers, - 'content-type': 'text/html', - }, - }, - ); - } - await writeSSRResult(request, response, incomingResponse); - return; - } - - // Apply the `status` override to the response object before responding. - // Response.status is read-only, so a clone is required to override. - if (response.status !== statusCode) { - response = new Response(response.body, { - status: statusCode, - headers: response.headers, - }); - } - await writeSSRResult(request, response, incomingResponse); - } - - match(request: Request, _allowPrerenderedRoutes: boolean): RouteData | undefined { - const url = new URL(request.url); - // ignore requests matching public assets - if (this.manifest.assets.has(url.pathname)) return undefined; - let pathname = prependForwardSlash(this.removeBase(url.pathname)); - - return this.manifestData.routes.find((route) => { - return ( - route.pattern.test(pathname) || - route.fallbackRoutes.some((fallbackRoute) => fallbackRoute.pattern.test(pathname)) - ); - }); - } - async renderError( request: Request, { locals, skipMiddleware = false, error, clientAddress, status }: RenderErrorOptions, @@ -397,8 +54,7 @@ export class DevApp extends BaseApp { } try { - const filePath500 = new URL(`./${custom500.component}`, this.settings.config.root); - const preloaded500Component = await this.pipeline.preload(custom500, filePath500); + const preloaded500Component = await this.pipeline.getComponentByRoute(custom500); const renderContext = await this.createRenderContext({ locals, pipeline: this.pipeline, @@ -432,126 +88,6 @@ export class DevApp extends BaseApp { } } -/** Check for /404 and /500 custom routes to compute status code */ -function getStatusByMatchedRoute(route: RouteData) { - if (route.route === '/404') return 404; - if (route.route === '/500') return 500; - return undefined; -} - -function isDefaultPrerendered404(route: RouteData) { - return route.route === '/404' && route.prerender && route.component === DEFAULT_404_COMPONENT; -} - -function isLoggedRequest(url: string) { - return url !== '/favicon.ico'; -} - -type HandleRequest = { - controller: DevServerController; - incomingRequest: http.IncomingMessage; - incomingResponse: http.ServerResponse; - isHttps: boolean; -}; - -type AsyncReturnType Promise> = T extends ( - ...args: any -) => Promise - ? R - : any; - -type HandleRoute = { - matchedRoute: AsyncReturnType; - url: URL; - pathname: string; - body: BodyInit | undefined; - incomingRequest: http.IncomingMessage; - incomingResponse: http.ServerResponse; -}; - -interface MatchedRoute { - route: RouteData; - filePath: URL; - resolvedPathname: string; -} - -async function matchRoute( - pathname: string, - routesList: RoutesList, - pipeline: DevPipeline, -): Promise { - const { config, logger, routeCache, serverLike, settings } = pipeline; - const matches = matchAllRoutes(pathname, routesList); - - const preloadedMatches = await getSortedPreloadedMatches({ pipeline, matches, settings }); - - for await (const { route: maybeRoute, filePath } of preloadedMatches) { - // attempt to get static paths - // if this fails, we have a bad URL match! - try { - await getProps({ - mod: await pipeline.preload(maybeRoute, filePath), - routeData: maybeRoute, - routeCache, - pathname: pathname, - logger, - serverLike, - base: config.base, - trailingSlash: config.trailingSlash, - }); - return { - route: maybeRoute, - filePath, - resolvedPathname: pathname, - }; - } catch (e) { - // Ignore error for no matching static paths - if (isAstroError(e) && e.title === NoMatchingStaticPathFound.title) { - continue; - } - throw e; - } - } - - // Try without `.html` extensions or `index.html` in request URLs to mimic - // routing behavior in production builds. This supports both file and directory - // build formats, and is necessary based on how the manifest tracks build targets. - const altPathname = pathname.replace(/\/index\.html$/, '/').replace(/\.html$/, ''); - - if (altPathname !== pathname) { - return await matchRoute(altPathname, routesList, pipeline); - } - - if (matches.length) { - const possibleRoutes = matches.flatMap((route) => route.component); - - logger.warn( - 'router', - `${NoMatchingStaticPathFound.message( - pathname, - )}\n\n${NoMatchingStaticPathFound.hint(possibleRoutes)}`, - ); - } - - const custom404 = getCustom404Route(routesList); - - if (custom404) { - const filePath = new URL(`./${custom404.component}`, config.root); - - return { - route: custom404, - filePath, - resolvedPathname: pathname, - }; - } - - return undefined; -} - -function getCustom404Route(manifestData: RoutesList): RouteData | undefined { - return manifestData.routes.find((r) => isRoute404(r.route)); -} - function getCustom500Route(manifestData: RoutesList): RouteData | undefined { return manifestData.routes.find((r) => isRoute500(r.route)); } diff --git a/packages/astro/src/core/app/dev/pipeline.ts b/packages/astro/src/core/app/dev/pipeline.ts index 40801eb3699e..bf3e83ca51b4 100644 --- a/packages/astro/src/core/app/dev/pipeline.ts +++ b/packages/astro/src/core/app/dev/pipeline.ts @@ -1,221 +1,97 @@ -import { fileURLToPath } from 'node:url'; -import { getInfoOutput } from '../../../cli/info/index.js'; -import type { AstroSettings, ComponentInstance, RoutesList } from '../../../types/astro.js'; +import type { ComponentInstance } from '../../../types/astro.js'; import type { - DevToolbarMetadata, RewritePayload, RouteData, SSRElement, - SSRLoadedRenderer, - SSRManifest, + SSRResult, } from '../../../types/public/index.js'; -import { getStylesForURL } from '../../../vite-plugin-astro-server/css.js'; -import { getComponentMetadata } from '../../../vite-plugin-astro-server/metadata.js'; -import { createResolve } from '../../../vite-plugin-astro-server/resolve.js'; -import { PAGE_SCRIPT_ID } from '../../../vite-plugin-scripts/index.js'; -import { type HeadElements, Pipeline, type TryRewriteResult } from '../../base-pipeline.js'; -import { ASTRO_VERSION } from '../../constants.js'; -import { enhanceViteSSRError } from '../../errors/dev/index.js'; -import { AggregateError, CSSError, MarkdownError } from '../../errors/index.js'; -import type { Logger } from '../../logger/core.js'; -import type { ModuleLoader } from '../../module-loader/index.js'; -import { RedirectComponentInstance, routeIsRedirect } from '../../redirects/index.js'; -import { loadRenderer } from '../../render/index.js'; -import { createDefaultRoutes } from '../../routing/default.js'; +import { Pipeline, type TryRewriteResult } from '../../base-pipeline.js'; +import { + createAssetLink, + createModuleScriptElement, + createStylesheetElementSet, +} from '../../render/ssr-element.js'; import { findRouteToRewrite } from '../../routing/rewrite.js'; -import { isPage, viteID } from '../../util.js'; -import { resolveIdToUrl } from '../../viteUtils.js'; export class DevPipeline extends Pipeline { - // renderers are loaded on every request, - // so it needs to be mutable here unlike in other environments - override renderers = new Array(); - - routesList: RoutesList | undefined; - - componentInterner: WeakMap = new WeakMap< - RouteData, - ComponentInstance - >(); - - private constructor( - readonly loader: ModuleLoader, - readonly logger: Logger, - readonly manifest: SSRManifest, - readonly settings: AstroSettings, - readonly config = settings.config, - readonly defaultRoutes = createDefaultRoutes(manifest), - ) { - const resolve = createResolve(loader, config.root); - const serverLike = settings.buildOutput === 'server'; - const streaming = true; - super(logger, manifest, 'development', [], resolve, serverLike, streaming); - manifest.serverIslandMap = settings.serverIslandMap; - manifest.serverIslandNameMap = settings.serverIslandNameMap; - } - - static create( - manifestData: RoutesList, - { - loader, + static create({ + logger, + manifest, + streaming, + }: Pick) { + const resolve = async function resolve(specifier: string) { + if (!(specifier in manifest.entryModules)) { + throw new Error(`Unable to resolve [${specifier}]`); + } + const bundlePath = manifest.entryModules[specifier]; + if (bundlePath.startsWith('data:') || bundlePath.length === 0) { + return bundlePath; + } else { + return createAssetLink(bundlePath, manifest.base, manifest.assetsPrefix); + } + }; + const pipeline = new DevPipeline( logger, manifest, - settings, - }: Pick, - ) { - const pipeline = new DevPipeline(loader, logger, manifest, settings); - pipeline.routesList = manifestData; + 'production', + manifest.renderers, + resolve, + true, + streaming, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + ); return pipeline; } - async headElements(routeData: RouteData): Promise { - const { - config: { root }, - loader, - runtimeMode, - settings, - } = this; - const filePath = new URL(`${routeData.component}`, root); + headElements(routeData: RouteData): Pick { + const routeInfo = this.manifest.routes.find((route) => route.routeData === routeData); + // may be used in the future for handling rel=modulepreload, rel=icon, rel=manifest etc. + const links = new Set(); const scripts = new Set(); - - // Inject HMR scripts - if (isPage(filePath, settings) && runtimeMode === 'development') { - scripts.add({ - props: { type: 'module', src: '/@vite/client' }, - children: '', - }); - - if ( - settings.config.devToolbar.enabled && - (await settings.preferences.get('devToolbar.enabled')) - ) { - const src = await resolveIdToUrl(loader, 'astro/runtime/client/dev-toolbar/entrypoint.js'); - scripts.add({ props: { type: 'module', src }, children: '' }); - - const additionalMetadata: DevToolbarMetadata['__astro_dev_toolbar__'] = { - root: fileURLToPath(settings.config.root), - version: ASTRO_VERSION, - latestAstroVersion: settings.latestAstroVersion, - debugInfo: await getInfoOutput({ userConfig: settings.config, print: false }), - }; - - // Additional data for the dev overlay - const children = `window.__astro_dev_toolbar__ = ${JSON.stringify(additionalMetadata)}`; - scripts.add({ props: {}, children }); - } - } - - // TODO: We should allow adding generic HTML elements to the head, not just scripts - for (const script of settings.scripts) { - if (script.stage === 'head-inline') { - scripts.add({ - props: {}, - children: script.content, - }); - } else if (script.stage === 'page' && isPage(filePath, settings)) { - scripts.add({ - props: { type: 'module', src: `/@id/${PAGE_SCRIPT_ID}` }, - children: '', - }); - } - } - - // Pass framework CSS in as style tags to be appended to the page. - const links = new Set(); - const { urls, styles: _styles } = await getStylesForURL(filePath, loader); - for (const href of urls) { - links.add({ props: { rel: 'stylesheet', href }, children: '' }); - } - - const styles = new Set(); - for (const { id, url: src, content } of _styles) { - // Vite handles HMR for styles injected as scripts - scripts.add({ props: { type: 'module', src }, children: '' }); - // But we still want to inject the styles to avoid FOUC. The style tags - // should emulate what Vite injects so further HMR works as expected. - styles.add({ props: { 'data-vite-dev-id': id }, children: content }); - } - - return { scripts, styles, links }; - } - - componentMetadata(routeData: RouteData) { - const { - config: { root }, - loader, - } = this; - const filePath = new URL(`${routeData.component}`, root); - return getComponentMetadata(filePath, loader); - } - - async preload(routeData: RouteData, filePath: URL) { - if (routeIsRedirect(routeData)) { - return RedirectComponentInstance; - } - - const { loader } = this; - - // First check built-in routes - for (const route of this.defaultRoutes) { - if (route.matchesComponent(filePath)) { - return route.instance; + const styles = createStylesheetElementSet(routeInfo?.styles ?? []); + + for (const script of routeInfo?.scripts ?? []) { + if ('stage' in script) { + if (script.stage === 'head-inline') { + scripts.add({ + props: {}, + children: script.children, + }); + } + } else { + scripts.add(createModuleScriptElement(script)); } } - - // Important: This needs to happen first, in case a renderer provides polyfills. - const renderers__ = this.settings.renderers.map((r) => loadRenderer(r, loader)); - const renderers_ = await Promise.all(renderers__); - this.renderers = renderers_.filter((r): r is SSRLoadedRenderer => Boolean(r)); - - try { - // Load the module from the Vite SSR Runtime. - const componentInstance = (await loader.import(viteID(filePath))) as ComponentInstance; - this.componentInterner.set(routeData, componentInstance); - return componentInstance; - } catch (error) { - // If the error came from Markdown or CSS, we already handled it and there's no need to enhance it - if (MarkdownError.is(error) || CSSError.is(error) || AggregateError.is(error)) { - throw error; - } - - throw enhanceViteSSRError({ error, filePath, loader }); - } + return { links, styles, scripts }; } - clearRouteCache() { - this.routeCache.clearAll(); - this.componentInterner = new WeakMap(); - } + componentMetadata() {} async getComponentByRoute(routeData: RouteData): Promise { - const component = this.componentInterner.get(routeData); - if (component) { - return component; - } else { - const filePath = new URL(`${routeData.component}`, this.config.root); - return await this.preload(routeData, filePath); - } + const url = new URL(routeData.component, this.manifest.rootDir); + const module = await import(url.toString()); + return module; } async tryRewrite(payload: RewritePayload, request: Request): Promise { - if (!this.routesList) { - throw new Error('Missing manifest data. This is an internal error, please file an issue.'); - } - const { routeData, pathname, newUrl } = findRouteToRewrite({ + const { newUrl, pathname, routeData } = findRouteToRewrite({ payload, request, - routes: this.routesList?.routes, - trailingSlash: this.config.trailingSlash, - buildFormat: this.config.build.format, - base: this.config.base, - outDir: this.manifest.outDir, + routes: this.manifest?.routes.map((r) => r.routeData), + trailingSlash: this.manifest.trailingSlash, + buildFormat: this.manifest.buildFormat, + base: this.manifest.base, + outDir: this.serverLike ? this.manifest.buildClientDir : this.manifest.outDir, }); const componentInstance = await this.getComponentByRoute(routeData); return { newUrl, pathname, componentInstance, routeData }; } - - setManifestData(manifestData: RoutesList) { - this.routesList = manifestData; - } } diff --git a/packages/astro/src/core/app/index.ts b/packages/astro/src/core/app/index.ts index 21e5d5638f43..f8908e264856 100644 --- a/packages/astro/src/core/app/index.ts +++ b/packages/astro/src/core/app/index.ts @@ -1,6 +1,8 @@ export { App } from './app.js'; export { BaseApp, type RenderErrorOptions, type RenderOptions } from './base.js'; export { deserializeManifest, fromRoutingStrategy, toRoutingStrategy } from './common.js'; +export { DevApp } from './dev/app.js'; +export { createConsoleLogger } from './logging.js'; export { deserializeRouteData, deserializeRouteInfo, diff --git a/packages/astro/src/core/app/logging.ts b/packages/astro/src/core/app/logging.ts new file mode 100644 index 000000000000..4b73d1c72e49 --- /dev/null +++ b/packages/astro/src/core/app/logging.ts @@ -0,0 +1,10 @@ +import type { AstroInlineConfig } from '../../types/public/index.js'; +import { consoleLogDestination } from '../logger/console.js'; +import { Logger } from '../logger/core.js'; + +export function createConsoleLogger(level: AstroInlineConfig['logLevel']): Logger { + return new Logger({ + dest: consoleLogDestination, + level: level ?? 'info', + }); +} diff --git a/packages/astro/src/core/app/types.ts b/packages/astro/src/core/app/types.ts index 6f25695ffcf7..04716ab07d13 100644 --- a/packages/astro/src/core/app/types.ts +++ b/packages/astro/src/core/app/types.ts @@ -51,7 +51,6 @@ export type AssetsPrefix = | undefined; export type SSRManifest = { - hrefRoot: string; adapterName: string; routes: RouteInfo[]; site?: string; @@ -86,12 +85,13 @@ export type SSRManifest = { actions?: () => Promise | SSRActions; checkOrigin: boolean; sessionConfig?: ResolvedSessionConfig; - cacheDir: string | URL; - srcDir: string | URL; - outDir: string | URL; - publicDir: string | URL; - buildClientDir: string | URL; - buildServerDir: string | URL; + cacheDir: URL; + srcDir: URL; + outDir: URL; + rootDir: URL; + publicDir: URL; + buildClientDir: URL; + buildServerDir: URL; csp: SSRManifestCSP | undefined; }; @@ -130,7 +130,21 @@ export type SerializedSSRManifest = Omit< | 'clientDirectives' | 'serverIslandNameMap' | 'key' + | 'rootDir' + | 'srcDir' + | 'cacheDir' + | 'outDir' + | 'publicDir' + | 'buildClientDir' + | 'buildServerDir' > & { + rootDir: string; + srcDir: string; + cacheDir: string; + outDir: string; + publicDir: string; + buildClientDir: string; + buildServerDir: string; routes: SerializedRouteInfo[]; assets: string[]; componentMetadata: [string, SSRComponentMetadata][]; diff --git a/packages/astro/src/core/build/generate.ts b/packages/astro/src/core/build/generate.ts index 9bc1c7a29b8c..7aee6097becd 100644 --- a/packages/astro/src/core/build/generate.ts +++ b/packages/astro/src/core/build/generate.ts @@ -710,7 +710,7 @@ async function createBuildManifest( }; } return { - hrefRoot: settings.config.root.toString(), + rootDir: settings.config.root, srcDir: settings.config.srcDir, buildClientDir: settings.config.build.client, buildServerDir: settings.config.build.server, diff --git a/packages/astro/src/core/build/plugins/plugin-manifest.ts b/packages/astro/src/core/build/plugins/plugin-manifest.ts index 0440992776be..0c390e177667 100644 --- a/packages/astro/src/core/build/plugins/plugin-manifest.ts +++ b/packages/astro/src/core/build/plugins/plugin-manifest.ts @@ -342,7 +342,7 @@ async function buildManifest( } return { - hrefRoot: opts.settings.config.root.toString(), + rootDir: opts.settings.config.root.toString(), cacheDir: opts.settings.config.cacheDir.toString(), outDir: opts.settings.config.outDir.toString(), srcDir: opts.settings.config.srcDir.toString(), diff --git a/packages/astro/src/core/config/index.ts b/packages/astro/src/core/config/index.ts index 00832e84733d..617d66768d30 100644 --- a/packages/astro/src/core/config/index.ts +++ b/packages/astro/src/core/config/index.ts @@ -3,7 +3,7 @@ export { resolveConfigPath, resolveRoot, } from './config.js'; -export { createNodeLogger } from './logging.js'; +export { createConsoleLogger, createNodeLogger } from './logging.js'; export { mergeConfig } from './merge.js'; export { createSettings } from './settings.js'; export { loadTSConfig, updateTSConfigForFramework } from './tsconfig.js'; diff --git a/packages/astro/src/core/config/logging.ts b/packages/astro/src/core/config/logging.ts index bd72f8b5e978..417ec87fb358 100644 --- a/packages/astro/src/core/config/logging.ts +++ b/packages/astro/src/core/config/logging.ts @@ -1,4 +1,5 @@ import type { AstroInlineConfig } from '../../types/public/config.js'; +import { consoleLogDestination } from '../logger/console.js'; import { Logger } from '../logger/core.js'; import { nodeLogDestination } from '../logger/node.js'; @@ -10,3 +11,10 @@ export function createNodeLogger(inlineConfig: AstroInlineConfig): Logger { level: inlineConfig.logLevel ?? 'info', }); } + +export function createConsoleLogger(level: AstroInlineConfig['logLevel']): Logger { + return new Logger({ + dest: consoleLogDestination, + level: level ?? 'info', + }); +} diff --git a/packages/astro/src/core/create-vite.ts b/packages/astro/src/core/create-vite.ts index 8a37377f2e34..5fbfc5015fbc 100644 --- a/packages/astro/src/core/create-vite.ts +++ b/packages/astro/src/core/create-vite.ts @@ -22,6 +22,7 @@ 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 type { AstroSettings } from '../types/astro.js'; +import { vitePluginApp } from '../vite-plugin-app/index.js'; import astroVitePlugin from '../vite-plugin-astro/index.js'; import astroPostprocessVitePlugin from '../vite-plugin-astro-postprocess/index.js'; import { vitePluginAstroServer } from '../vite-plugin-astro-server/index.js'; @@ -151,6 +152,7 @@ export async function createVite( astroScriptsPlugin({ settings }), // The server plugin is for dev only and having it run during the build causes // the build to run very slow as the filewatcher is triggered often. + command === 'dev' && vitePluginApp(), command === 'dev' && vitePluginAstroServer({ settings, logger }), importMetaEnv({ envLoader }), astroEnv({ settings, sync, envLoader }), diff --git a/packages/astro/src/core/errors/dev/runtime.ts b/packages/astro/src/core/errors/dev/runtime.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/astro/src/core/errors/dev/utils/runtime.ts b/packages/astro/src/core/errors/dev/utils/runtime.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/astro/src/core/routing/default.ts b/packages/astro/src/core/routing/default.ts index 000e4cd1a03b..52a838bd4fc8 100644 --- a/packages/astro/src/core/routing/default.ts +++ b/packages/astro/src/core/routing/default.ts @@ -18,7 +18,7 @@ type DefaultRouteParams = { export const DEFAULT_COMPONENTS = [DEFAULT_404_COMPONENT, SERVER_ISLAND_COMPONENT]; export function createDefaultRoutes(manifest: SSRManifest): DefaultRouteParams[] { - const root = new URL(manifest.hrefRoot); + const root = new URL(manifest.rootDir); return [ { instance: default404Instance, diff --git a/packages/astro/src/manifest/serialized.ts b/packages/astro/src/manifest/serialized.ts index 013ff1904a37..6e45885bf3cc 100644 --- a/packages/astro/src/manifest/serialized.ts +++ b/packages/astro/src/manifest/serialized.ts @@ -46,9 +46,7 @@ export async function serializedManifestPlugin({ }; } -export async function createSerializedManifest( - settings: AstroSettings, -): Promise { +async function createSerializedManifest(settings: AstroSettings): Promise { let i18nManifest: SSRManifestI18n | undefined; let csp: SSRManifestCSP | undefined; if (settings.config.i18n) { @@ -78,13 +76,13 @@ export async function createSerializedManifest( } return { - hrefRoot: settings.config.root.toString(), - srcDir: settings.config.srcDir, - cacheDir: settings.config.cacheDir, - outDir: settings.config.outDir, - buildServerDir: settings.config.build.server, - buildClientDir: settings.config.build.client, - publicDir: settings.config.publicDir, + rootDir: settings.config.root.toString(), + srcDir: settings.config.srcDir.toString(), + cacheDir: settings.config.cacheDir.toString(), + outDir: settings.config.outDir.toString(), + buildServerDir: settings.config.build.server.toString(), + buildClientDir: settings.config.build.client.toString(), + publicDir: settings.config.publicDir.toString(), trailingSlash: settings.config.trailingSlash, buildFormat: settings.config.build.format, compressHTML: settings.config.compressHTML, diff --git a/packages/astro/src/prerender/routing.ts b/packages/astro/src/prerender/routing.ts index 459330142cbb..e78ac53777d1 100644 --- a/packages/astro/src/prerender/routing.ts +++ b/packages/astro/src/prerender/routing.ts @@ -1,25 +1,23 @@ -import type { DevPipeline } from '../core/app/dev/pipeline.js'; import { routeIsRedirect } from '../core/redirects/index.js'; import { routeComparator } from '../core/routing/priority.js'; -import type { AstroSettings } from '../types/astro.js'; -import type { RouteData } from '../types/public/internal.js'; -import { getPrerenderStatus } from './metadata.js'; +import type { RouteData, SSRManifest } from '../types/public/internal.js'; +import type { AstroServerPipeline } from '../vite-plugin-app/pipeline.js'; type GetSortedPreloadedMatchesParams = { - pipeline: DevPipeline; + pipeline: AstroServerPipeline; matches: RouteData[]; - settings: AstroSettings; + manifest: SSRManifest; }; export async function getSortedPreloadedMatches({ pipeline, matches, - settings, + manifest, }: GetSortedPreloadedMatchesParams) { return ( await preloadAndSetPrerenderStatus({ pipeline, matches, - settings, + manifest, }) ) .sort((a, b) => routeComparator(a.route, b.route)) @@ -27,9 +25,9 @@ export async function getSortedPreloadedMatches({ } type PreloadAndSetPrerenderStatusParams = { - pipeline: DevPipeline; + pipeline: AstroServerPipeline; matches: RouteData[]; - settings: AstroSettings; + manifest: SSRManifest; }; type PreloadAndSetPrerenderStatusResult = { @@ -38,13 +36,12 @@ type PreloadAndSetPrerenderStatusResult = { }; async function preloadAndSetPrerenderStatus({ - pipeline, matches, - settings, + manifest, }: PreloadAndSetPrerenderStatusParams): Promise { const preloaded = new Array(); for (const route of matches) { - const filePath = new URL(`./${route.component}`, settings.config.root); + const filePath = new URL(`./${route.component}`, manifest.rootDir); if (routeIsRedirect(route)) { preloaded.push({ route, @@ -53,16 +50,6 @@ async function preloadAndSetPrerenderStatus({ continue; } - // gets the prerender metadata set by the `astro:scanner` vite plugin - const prerenderStatus = getPrerenderStatus({ - filePath, - loader: pipeline.loader, - }); - - if (prerenderStatus !== undefined) { - route.prerender = prerenderStatus; - } - preloaded.push({ route, filePath }); } return preloaded; diff --git a/packages/astro/src/types/public/index.ts b/packages/astro/src/types/public/index.ts index 42c131fc2b68..2b31f34ea595 100644 --- a/packages/astro/src/types/public/index.ts +++ b/packages/astro/src/types/public/index.ts @@ -21,7 +21,12 @@ export type { UnresolvedImageTransform, } from '../../assets/types.js'; export type { ContainerRenderer } from '../../container/index.js'; -export type { AssetsPrefix, NodeAppHeadersJson, SSRManifest } from '../../core/app/types.js'; +export type { + AssetsPrefix, + NodeAppHeadersJson, + RouteInfo, + SSRManifest, +} from '../../core/app/types.js'; export type { AstroCookieGetOptions, AstroCookieSetOptions, diff --git a/packages/astro/src/vite-plugin-app/app.ts b/packages/astro/src/vite-plugin-app/app.ts new file mode 100644 index 000000000000..3ac168b6e7dd --- /dev/null +++ b/packages/astro/src/vite-plugin-app/app.ts @@ -0,0 +1,573 @@ +import type http from 'node:http'; +import { prependForwardSlash, removeTrailingForwardSlash } from '@astrojs/internal-helpers/path'; +import { loadActions } from '../actions/loadActions.js'; +import { BaseApp, type RenderErrorOptions } from '../core/app/index.js'; +import { shouldAppendForwardSlash } from '../core/build/util.js'; +import { + clientLocalsSymbol, + DEFAULT_404_COMPONENT, + NOOP_MIDDLEWARE_HEADER, + REROUTE_DIRECTIVE_HEADER, + REWRITE_DIRECTIVE_HEADER_KEY, +} from '../core/constants.js'; +import { + MiddlewareNoDataOrNextCalled, + MiddlewareNotAResponse, + NoMatchingStaticPathFound, +} from '../core/errors/errors-data.js'; +import { type AstroError, createSafeError, isAstroError } from '../core/errors/index.js'; +import type { Logger } from '../core/logger/core.js'; +import { req } from '../core/messages.js'; +import { loadMiddleware } from '../core/middleware/loadMiddleware.js'; +import type { ModuleLoader } from '../core/module-loader/index.js'; +import { routeIsRedirect } from '../core/redirects/index.js'; +import { getProps } from '../core/render/index.js'; +import type { CreateRenderContext, RenderContext } from '../core/render-context.js'; +import { createRequest } from '../core/request.js'; +import { redirectTemplate } from '../core/routing/3xx.js'; +import { matchAllRoutes } from '../core/routing/index.js'; +import { isRoute404, isRoute500 } from '../core/routing/match.js'; +import { PERSIST_SYMBOL } from '../core/session.js'; +import { getSortedPreloadedMatches } from '../prerender/routing.js'; +import type { AstroSettings, RoutesList } from '../types/astro.js'; +import type { RouteData, SSRManifest } from '../types/public/index.js'; +import type { DevServerController } from '../vite-plugin-astro-server/controller.js'; +import { recordServerError } from '../vite-plugin-astro-server/error.js'; +import { runWithErrorHandling } from '../vite-plugin-astro-server/index.js'; +import { + handle500Response, + writeSSRResult, + writeWebResponse, +} from '../vite-plugin-astro-server/response.js'; +import { AstroServerPipeline } from './pipeline.js'; + +export class AstroServerApp extends BaseApp { + settings: AstroSettings; + logger: Logger; + loader: ModuleLoader; + manifestData: RoutesList; + currentRenderContext: RenderContext | undefined = undefined; + constructor( + manifest: SSRManifest, + streaming = true, + logger: Logger, + manifestData: RoutesList, + loader: ModuleLoader, + settings: AstroSettings, + ) { + super(manifest, streaming, settings, logger, loader, manifestData); + this.settings = settings; + this.logger = logger; + this.loader = loader; + this.manifestData = manifestData; + } + + static async create( + manifest: SSRManifest, + routesList: RoutesList, + logger: Logger, + loader: ModuleLoader, + settings: AstroSettings, + ): Promise { + return new AstroServerApp(manifest, true, logger, routesList, loader, settings); + } + + createPipeline( + _streaming: boolean, + manifest: SSRManifest, + settings: AstroSettings, + logger: Logger, + loader: ModuleLoader, + manifestData: RoutesList, + ): AstroServerPipeline { + return AstroServerPipeline.create(manifestData, { + loader, + logger, + manifest, + settings, + }); + } + + async createRenderContext(payload: CreateRenderContext): Promise { + this.currentRenderContext = await super.createRenderContext(payload); + return this.currentRenderContext; + } + + public clearRouteCache() { + this.pipeline.clearRouteCache(); + } + + public async handleRequest({ + controller, + incomingRequest, + incomingResponse, + isHttps, + }: HandleRequest): Promise { + const origin = `${isHttps ? 'https' : 'http'}://${ + incomingRequest.headers[':authority'] ?? incomingRequest.headers.host + }`; + + const url = new URL(origin + incomingRequest.url); + let pathname: string; + if (this.manifest.trailingSlash === 'never' && !incomingRequest.url) { + pathname = ''; + } else { + // We already have a middleware that checks if there's an incoming URL that has invalid URI, so it's safe + // to not handle the error: packages/astro/src/vite-plugin-astro-server/base.ts + pathname = decodeURI(url.pathname); + } + + // Add config.base back to url before passing it to SSR + url.pathname = removeTrailingForwardSlash(this.manifest.base) + url.pathname; + if ( + url.pathname.endsWith('/') && + !shouldAppendForwardSlash(this.manifest.trailingSlash, this.manifest.buildFormat) + ) { + url.pathname = url.pathname.slice(0, -1); + } + + let body: BodyInit | undefined = undefined; + if (!(incomingRequest.method === 'GET' || incomingRequest.method === 'HEAD')) { + let bytes: Uint8Array[] = []; + await new Promise((resolve) => { + incomingRequest.on('data', (part) => { + bytes.push(part); + }); + incomingRequest.on('end', resolve); + }); + body = Buffer.concat(bytes); + } + + const self = this; + await runWithErrorHandling({ + controller, + pathname, + async run() { + const matchedRoute = await matchRoute( + pathname, + self.manifestData, + self.pipeline, + self.manifest, + ); + const resolvedPathname = matchedRoute?.resolvedPathname ?? pathname; + return await self.handleRoute({ + matchedRoute, + url, + pathname: resolvedPathname, + body, + incomingRequest: incomingRequest, + incomingResponse: incomingResponse, + }); + }, + onError(_err) { + const error = createSafeError(_err); + if (self.loader) { + const { errorWithMetadata } = recordServerError( + self.loader, + self.manifest, + self.logger, + error, + ); + handle500Response(self.loader, incomingResponse, errorWithMetadata); + } + return error; + }, + }); + } + + async handleRoute({ + matchedRoute, + incomingRequest, + incomingResponse, + body, + url, + pathname, + }: HandleRoute): Promise { + const timeStart = performance.now(); + const { loader, logger } = this.pipeline; + + if (!matchedRoute) { + // This should never happen, because ensure404Route will add a 404 route if none exists. + throw new Error('No route matched, and default 404 route was not found.'); + } + + let request: Request; + let renderContext: RenderContext; + let route: RouteData = matchedRoute.route; + const componentInstance = await this.pipeline.getComponentByRoute(route); + const actions = await loadActions(loader); + this.pipeline.setActions(actions); + const middleware = (await loadMiddleware(loader)).onRequest; + // This is required for adapters to set locals in dev mode. They use a dev server middleware to inject locals to the `http.IncomingRequest` object. + const locals = Reflect.get(incomingRequest, clientLocalsSymbol); + + // Allows adapters to pass in locals in dev mode. + request = createRequest({ + url, + headers: incomingRequest.headers, + method: incomingRequest.method, + body, + logger, + isPrerendered: route.prerender, + routePattern: route.component, + }); + + // Set user specified headers to response object. + for (const [name, value] of Object.entries(this.settings.config.server.headers ?? {})) { + if (value) incomingResponse.setHeader(name, value); + } + + renderContext = await this.createRenderContext({ + locals, + pipeline: this.pipeline, + pathname, + middleware: isDefaultPrerendered404(matchedRoute.route) ? undefined : middleware, + request, + routeData: route, + clientAddress: incomingRequest.socket.remoteAddress, + actions, + shouldInjectCspMetaTags: false, + }); + + let response; + let statusCode = 200; + let isReroute = false; + let isRewrite = false; + + try { + response = await renderContext.render(componentInstance); + isReroute = response.headers.has(REROUTE_DIRECTIVE_HEADER); + isRewrite = response.headers.has(REWRITE_DIRECTIVE_HEADER_KEY); + const statusCodedMatched = getStatusByMatchedRoute(route); + statusCode = isRewrite + ? // Ignore `matchedRoute` status for rewrites + response.status + : // Our internal noop middleware sets a particular header. If the header isn't present, it means that the user have + // their own middleware, so we need to return what the user returns. + !response.headers.has(NOOP_MIDDLEWARE_HEADER) && !isReroute + ? response.status + : (statusCodedMatched ?? response.status); + } catch (err: any) { + response = await this.renderError(request, { + skipMiddleware: false, + locals, + status: 500, + prerenderedErrorPageFetch: fetch, + clientAddress: incomingRequest.socket.remoteAddress, + error: err, + }); + statusCode = 500; + } finally { + this.currentRenderContext?.session?.[PERSIST_SYMBOL](); + } + + if (isLoggedRequest(pathname)) { + const timeEnd = performance.now(); + logger.info( + null, + req({ + url: pathname, + method: incomingRequest.method, + statusCode, + isRewrite, + reqTime: timeEnd - timeStart, + }), + ); + } + + if ( + statusCode === 404 && + // If the body isn't null, that means the user sets the 404 status + // but uses the current route to handle the 404 + response.body === null && + response.headers.get(REROUTE_DIRECTIVE_HEADER) !== 'no' + ) { + const fourOhFourRoute = await matchRoute( + '/404', + this.manifestData, + this.pipeline, + this.manifest, + ); + if (fourOhFourRoute) { + renderContext = await this.createRenderContext({ + locals, + pipeline: this.pipeline, + pathname, + middleware: isDefaultPrerendered404(fourOhFourRoute.route) ? undefined : middleware, + request, + routeData: fourOhFourRoute.route, + clientAddress: incomingRequest.socket.remoteAddress, + status: 404, + shouldInjectCspMetaTags: false, + }); + const component = await this.pipeline.preload( + fourOhFourRoute.route, + fourOhFourRoute.filePath, + ); + response = await renderContext.render(component); + } + } + + // We remove the internally-used header before we send the response to the user agent. + if (isReroute) { + response.headers.delete(REROUTE_DIRECTIVE_HEADER); + } + if (isRewrite) { + response.headers.delete(REROUTE_DIRECTIVE_HEADER); + } + + if (route.type === 'endpoint') { + await writeWebResponse(incomingResponse, response); + return; + } + + // This check is important in case of rewrites. + // A route can start with a 404 code, then the rewrite kicks in and can return a 200 status code + if (isRewrite) { + await writeSSRResult(request, response, incomingResponse); + return; + } + + // We are in a recursion, and it's possible that this function is called itself with a status code + // By default, the status code passed via parameters is computed by the matched route. + // + // By default, we should give priority to the status code passed, although it's possible that + // the `Response` emitted by the user is a redirect. If so, then return the returned response. + if (response.status < 400 && response.status >= 300) { + if ( + response.status >= 300 && + response.status < 400 && + routeIsRedirect(route) && + !this.settings.config.build.redirects && + this.settings.buildOutput === 'static' + ) { + // If we're here, it means that the calling static redirect that was configured by the user + // We try to replicate the same behaviour that we provide during a static build + const location = response.headers.get('location')!; + response = new Response( + redirectTemplate({ + status: response.status, + absoluteLocation: location, + relativeLocation: location, + from: pathname, + }), + { + status: 200, + headers: { + ...response.headers, + 'content-type': 'text/html', + }, + }, + ); + } + await writeSSRResult(request, response, incomingResponse); + return; + } + + // Apply the `status` override to the response object before responding. + // Response.status is read-only, so a clone is required to override. + if (response.status !== statusCode) { + response = new Response(response.body, { + status: statusCode, + headers: response.headers, + }); + } + await writeSSRResult(request, response, incomingResponse); + } + + match(request: Request, _allowPrerenderedRoutes: boolean): RouteData | undefined { + const url = new URL(request.url); + // ignore requests matching public assets + if (this.manifest.assets.has(url.pathname)) return undefined; + let pathname = prependForwardSlash(this.removeBase(url.pathname)); + + return this.manifestData.routes.find((route) => { + return ( + route.pattern.test(pathname) || + route.fallbackRoutes.some((fallbackRoute) => fallbackRoute.pattern.test(pathname)) + ); + }); + } + + async renderError( + request: Request, + { locals, skipMiddleware = false, error, clientAddress, status }: RenderErrorOptions, + ): Promise { + // we always throw when we have Astro errors around the middleware + if ( + isAstroError(error) && + [MiddlewareNoDataOrNextCalled.name, MiddlewareNotAResponse.name].includes(error.name) + ) { + throw error; + } + + const custom500 = getCustom500Route(this.manifestData); + // Show dev overlay + if (!custom500) { + throw error; + } + + try { + const filePath500 = new URL(`./${custom500.component}`, this.manifest.rootDir); + const preloaded500Component = await this.pipeline.preload(custom500, filePath500); + const renderContext = await this.createRenderContext({ + locals, + pipeline: this.pipeline, + pathname: this.getPathnameFromRequest(request), + middleware: skipMiddleware ? undefined : await this.pipeline.getMiddleware(), + request, + routeData: custom500, + clientAddress, + actions: await this.pipeline.getActions(), + status, + shouldInjectCspMetaTags: false, + }); + renderContext.props.error = error; + const response = await renderContext.render(preloaded500Component); + // Log useful information that the custom 500 page may not display unlike the default error overlay + this.logger.error('router', (error as AstroError).stack || (error as AstroError).message); + return response; + } catch (_err) { + if (skipMiddleware === false) { + return this.renderError(request, { + clientAddress: undefined, + prerenderedErrorPageFetch: fetch, + status: 500, + skipMiddleware: true, + error: _err, + }); + } + // If even skipping the middleware isn't enough to prevent the error, show the dev overlay + throw _err; + } + } +} + +/** Check for /404 and /500 custom routes to compute status code */ +function getStatusByMatchedRoute(route: RouteData) { + if (route.route === '/404') return 404; + if (route.route === '/500') return 500; + return undefined; +} + +function isDefaultPrerendered404(route: RouteData) { + return route.route === '/404' && route.prerender && route.component === DEFAULT_404_COMPONENT; +} + +function isLoggedRequest(url: string) { + return url !== '/favicon.ico'; +} + +type HandleRequest = { + controller: DevServerController; + incomingRequest: http.IncomingMessage; + incomingResponse: http.ServerResponse; + isHttps: boolean; +}; + +type AsyncReturnType Promise> = T extends ( + ...args: any +) => Promise + ? R + : any; + +type HandleRoute = { + matchedRoute: AsyncReturnType; + url: URL; + pathname: string; + body: BodyInit | undefined; + incomingRequest: http.IncomingMessage; + incomingResponse: http.ServerResponse; +}; + +interface MatchedRoute { + route: RouteData; + filePath: URL; + resolvedPathname: string; +} + +async function matchRoute( + pathname: string, + routesList: RoutesList, + pipeline: AstroServerPipeline, + manifest: SSRManifest, +): Promise { + const { logger, routeCache, serverLike } = pipeline; + const matches = matchAllRoutes(pathname, routesList); + + const preloadedMatches = await getSortedPreloadedMatches({ + pipeline, + matches, + manifest, + }); + + for await (const { route: maybeRoute, filePath } of preloadedMatches) { + // attempt to get static paths + // if this fails, we have a bad URL match! + try { + await getProps({ + mod: await pipeline.preload(maybeRoute, filePath), + routeData: maybeRoute, + routeCache, + pathname: pathname, + logger, + serverLike, + base: manifest.base, + trailingSlash: manifest.trailingSlash, + }); + return { + route: maybeRoute, + filePath, + resolvedPathname: pathname, + }; + } catch (e) { + // Ignore error for no matching static paths + if (isAstroError(e) && e.title === NoMatchingStaticPathFound.title) { + continue; + } + throw e; + } + } + + // Try without `.html` extensions or `index.html` in request URLs to mimic + // routing behavior in production builds. This supports both file and directory + // build formats, and is necessary based on how the manifest tracks build targets. + const altPathname = pathname.replace(/\/index\.html$/, '/').replace(/\.html$/, ''); + + if (altPathname !== pathname) { + return await matchRoute(altPathname, routesList, pipeline, manifest); + } + + if (matches.length) { + const possibleRoutes = matches.flatMap((route) => route.component); + + logger.warn( + 'router', + `${NoMatchingStaticPathFound.message( + pathname, + )}\n\n${NoMatchingStaticPathFound.hint(possibleRoutes)}`, + ); + } + + const custom404 = getCustom404Route(routesList); + + if (custom404) { + const filePath = new URL(`./${custom404.component}`, manifest.rootDir); + + return { + route: custom404, + filePath, + resolvedPathname: pathname, + }; + } + + return undefined; +} + +function getCustom404Route(manifestData: RoutesList): RouteData | undefined { + return manifestData.routes.find((r) => isRoute404(r.route)); +} + +function getCustom500Route(manifestData: RoutesList): RouteData | undefined { + return manifestData.routes.find((r) => isRoute500(r.route)); +} diff --git a/packages/astro/src/core/app/dev/index.ts b/packages/astro/src/vite-plugin-app/createExports.ts similarity index 50% rename from packages/astro/src/core/app/dev/index.ts rename to packages/astro/src/vite-plugin-app/createExports.ts index 6eb424ea3cd5..75679d37c1a2 100644 --- a/packages/astro/src/core/app/dev/index.ts +++ b/packages/astro/src/vite-plugin-app/createExports.ts @@ -1,21 +1,20 @@ // @ts-expect-error This is a virtual module import { routes } from 'astro:routes'; // @ts-expect-error This is a virtual module -import { manifest } from 'astro:serialized-manifest'; -import type http from 'node:http'; -import type { AstroSettings, RoutesList } from '../../../types/astro.js'; -import type { DevServerController } from '../../../vite-plugin-astro-server/controller.js'; -import { Logger } from '../../logger/core.js'; -import { nodeLogDestination } from '../../logger/node.js'; -import type { ModuleLoader } from '../../module-loader/index.js'; -import type { RouteInfo } from '../types.js'; -import { DevApp } from './app.js'; +import { manifest as serializedManifest } from 'astro:serialized-manifest'; -export { DevApp }; +import type http from 'node:http'; +import type { RouteInfo } from '../core/app/types.js'; +import { Logger } from '../core/logger/core.js'; +import { nodeLogDestination } from '../core/logger/node.js'; +import type { ModuleLoader } from '../core/module-loader/index.js'; +import type { AstroSettings, RoutesList } from '../types/astro.js'; +import type { DevServerController } from '../vite-plugin-astro-server/controller.js'; +import { AstroServerApp } from './app.js'; export default async function createExports( - settings: AstroSettings, controller: DevServerController, + settings: AstroSettings, loader: ModuleLoader, ) { const logger = new Logger({ @@ -23,15 +22,15 @@ export default async function createExports( level: 'info', }); const routesList: RoutesList = { routes: routes.map((r: RouteInfo) => r.routeData) }; - const app = await DevApp.create(manifest, routesList, settings, logger, loader); + const app = await AstroServerApp.create(serializedManifest, routesList, logger, loader, settings); return { handler(incomingRequest: http.IncomingMessage, incomingResponse: http.ServerResponse) { app.handleRequest({ controller, incomingRequest, incomingResponse, - isHttps: loader.isHttps(), + isHttps: loader?.isHttps() ?? false, }); }, }; diff --git a/packages/astro/src/vite-plugin-app/index.ts b/packages/astro/src/vite-plugin-app/index.ts new file mode 100644 index 000000000000..10a4a52afd25 --- /dev/null +++ b/packages/astro/src/vite-plugin-app/index.ts @@ -0,0 +1,16 @@ +import type * as vite from 'vite'; + +const VIRTUAL_MODULE_ID = 'astro:app'; + +export function vitePluginApp(): vite.Plugin { + return { + name: 'astro:app', + + async resolveId(id) { + if (id === VIRTUAL_MODULE_ID) { + const url = new URL('./createExports.js', import.meta.url); + return await this.resolve(url.toString()); + } + }, + }; +} diff --git a/packages/astro/src/vite-plugin-app/pipeline.ts b/packages/astro/src/vite-plugin-app/pipeline.ts new file mode 100644 index 000000000000..5a494f93d07d --- /dev/null +++ b/packages/astro/src/vite-plugin-app/pipeline.ts @@ -0,0 +1,221 @@ +import { fileURLToPath } from 'node:url'; +import { getInfoOutput } from '../cli/info/index.js'; +import { type HeadElements, Pipeline, type TryRewriteResult } from '../core/base-pipeline.js'; +import { ASTRO_VERSION } from '../core/constants.js'; +import { enhanceViteSSRError } from '../core/errors/dev/index.js'; +import { AggregateError, CSSError, MarkdownError } from '../core/errors/index.js'; +import type { Logger } from '../core/logger/core.js'; +import type { ModuleLoader } from '../core/module-loader/index.js'; +import { RedirectComponentInstance, routeIsRedirect } from '../core/redirects/index.js'; +import { loadRenderer } from '../core/render/index.js'; +import { createDefaultRoutes } from '../core/routing/default.js'; +import { findRouteToRewrite } from '../core/routing/rewrite.js'; +import { isPage } from '../core/util.js'; +import { resolveIdToUrl } from '../core/viteUtils.js'; +import type { AstroSettings, ComponentInstance, RoutesList } from '../types/astro.js'; +import type { + DevToolbarMetadata, + RewritePayload, + RouteData, + SSRElement, + SSRLoadedRenderer, + SSRManifest, +} from '../types/public/index.js'; +import { getStylesForURL } from '../vite-plugin-astro-server/css.js'; +import { getComponentMetadata } from '../vite-plugin-astro-server/metadata.js'; +import { createResolve } from '../vite-plugin-astro-server/resolve.js'; +import { PAGE_SCRIPT_ID } from '../vite-plugin-scripts/index.js'; + +export class AstroServerPipeline extends Pipeline { + // renderers are loaded on every request, + // so it needs to be mutable here unlike in other environments + override renderers = new Array(); + + routesList: RoutesList | undefined; + + componentInterner: WeakMap = new WeakMap< + RouteData, + ComponentInstance + >(); + + private constructor( + readonly loader: ModuleLoader, + readonly logger: Logger, + readonly manifest: SSRManifest, + readonly settings: AstroSettings | undefined, + readonly defaultRoutes = createDefaultRoutes(manifest), + ) { + const resolve = createResolve(loader, manifest.rootDir); + const serverLike = settings?.buildOutput === 'server'; + const streaming = true; + super(logger, manifest, 'development', [], resolve, serverLike, streaming); + if (settings) { + manifest.serverIslandMap = settings.serverIslandMap; + manifest.serverIslandNameMap = settings.serverIslandNameMap; + } + } + + static create( + manifestData: RoutesList, + { + loader, + logger, + manifest, + settings, + }: Pick + + ) { + const pipeline = new AstroServerPipeline(loader, logger, manifest, settings); + pipeline.routesList = manifestData; + return pipeline; + } + + async headElements(routeData: RouteData): Promise { + const { manifest, loader, runtimeMode, settings } = this; + const filePath = new URL(`${routeData.component}`, manifest.rootDir); + const scripts = new Set(); + + // Inject HMR scripts + if (settings) { + if (isPage(filePath, settings) && runtimeMode === 'development') { + scripts.add({ + props: { type: 'module', src: '/@vite/client' }, + children: '', + }); + + if ( + settings.config.devToolbar.enabled && + (await settings.preferences.get('devToolbar.enabled')) + ) { + const src = await resolveIdToUrl( + loader, + 'astro/runtime/client/dev-toolbar/entrypoint.js', + ); + scripts.add({ props: { type: 'module', src }, children: '' }); + + const additionalMetadata: DevToolbarMetadata['__astro_dev_toolbar__'] = { + root: fileURLToPath(settings.config.root), + version: ASTRO_VERSION, + latestAstroVersion: settings.latestAstroVersion, + debugInfo: await getInfoOutput({ userConfig: settings.config, print: false }), + }; + + // Additional data for the dev overlay + const children = `window.__astro_dev_toolbar__ = ${JSON.stringify(additionalMetadata)}`; + scripts.add({ props: {}, children }); + } + } + + // TODO: We should allow adding generic HTML elements to the head, not just scripts + for (const script of settings.scripts) { + if (script.stage === 'head-inline') { + scripts.add({ + props: {}, + children: script.content, + }); + } else if (script.stage === 'page' && isPage(filePath, settings)) { + scripts.add({ + props: { type: 'module', src: `/@id/${PAGE_SCRIPT_ID}` }, + children: '', + }); + } + } + } + + // Pass framework CSS in as style tags to be appended to the page. + const links = new Set(); + const { urls, styles: _styles } = await getStylesForURL(filePath, loader); + for (const href of urls) { + links.add({ props: { rel: 'stylesheet', href }, children: '' }); + } + + const styles = new Set(); + for (const { id, url: src, content } of _styles) { + // Vite handles HMR for styles injected as scripts + scripts.add({ props: { type: 'module', src }, children: '' }); + // But we still want to inject the styles to avoid FOUC. The style tags + // should emulate what Vite injects so further HMR works as expected. + styles.add({ props: { 'data-vite-dev-id': id }, children: content }); + } + + return { scripts, styles, links }; + } + + componentMetadata(routeData: RouteData) { + const filePath = new URL(`${routeData.component}`, this.manifest.rootDir); + return getComponentMetadata(filePath, this.loader); + } + + async preload(routeData: RouteData, filePath: URL) { + if (routeIsRedirect(routeData)) { + return RedirectComponentInstance; + } + + const { loader } = this; + + // First check built-in routes + for (const route of this.defaultRoutes) { + if (route.matchesComponent(filePath)) { + return route.instance; + } + } + + // Important: This needs to happen first, in case a renderer provides polyfills. + if (this.settings) { + const renderers__ = this.settings.renderers.map((r) => loadRenderer(r, loader)); + const renderers_ = await Promise.all(renderers__); + this.renderers = renderers_.filter((r): r is SSRLoadedRenderer => Boolean(r)); + } + + try { + // Load the module from the Vite SSR Runtime. + const componentInstance = (await loader.import(filePath.toString())) as ComponentInstance; + this.componentInterner.set(routeData, componentInstance); + return componentInstance; + } catch (error) { + // If the error came from Markdown or CSS, we already handled it and there's no need to enhance it + if (MarkdownError.is(error) || CSSError.is(error) || AggregateError.is(error)) { + throw error; + } + + throw enhanceViteSSRError({ error, filePath, loader }); + } + } + + clearRouteCache() { + this.routeCache.clearAll(); + this.componentInterner = new WeakMap(); + } + + async getComponentByRoute(routeData: RouteData): Promise { + const component = this.componentInterner.get(routeData); + if (component) { + return component; + } else { + const filePath = new URL(`${routeData.component}`, this.manifest.rootDir); + return await this.preload(routeData, filePath); + } + } + + async tryRewrite(payload: RewritePayload, request: Request): Promise { + if (!this.routesList) { + throw new Error('Missing manifest data. This is an internal error, please file an issue.'); + } + const { routeData, pathname, newUrl } = findRouteToRewrite({ + payload, + request, + routes: this.routesList?.routes, + trailingSlash: this.manifest.trailingSlash, + buildFormat: this.manifest.buildFormat, + base: this.manifest.base, + outDir: this.manifest.outDir, + }); + + const componentInstance = await this.getComponentByRoute(routeData); + return { newUrl, pathname, componentInstance, routeData }; + } + + setManifestData(manifestData: RoutesList) { + this.routesList = manifestData; + } +} diff --git a/packages/astro/src/vite-plugin-astro-server/controller.ts b/packages/astro/src/vite-plugin-astro-server/controller.ts index 06c8796e7e39..848786a580b3 100644 --- a/packages/astro/src/vite-plugin-astro-server/controller.ts +++ b/packages/astro/src/vite-plugin-astro-server/controller.ts @@ -88,7 +88,7 @@ interface RunWithErrorHandlingParams { controller: DevServerController; pathname: string; run: () => Promise; - onError: (error: unknown) => Error; + onError: (error: unknown) => Error | undefined; } export async function runWithErrorHandling({ diff --git a/packages/astro/src/vite-plugin-astro-server/error.ts b/packages/astro/src/vite-plugin-astro-server/error.ts index cd3dedeb4cc0..3cb04e914da4 100644 --- a/packages/astro/src/vite-plugin-astro-server/error.ts +++ b/packages/astro/src/vite-plugin-astro-server/error.ts @@ -1,18 +1,15 @@ +import type { SSRManifest } from '../core/app/types.js'; import { collectErrorMetadata } from '../core/errors/dev/index.js'; -import { createSafeError } from '../core/errors/index.js'; import type { Logger } from '../core/logger/core.js'; import { formatErrorMessage } from '../core/messages.js'; import type { ModuleLoader } from '../core/module-loader/index.js'; -import type { AstroConfig } from '../types/public/config.js'; export function recordServerError( loader: ModuleLoader, - config: AstroConfig, + manifest: SSRManifest, logger: Logger, - _err: unknown, + err: Error, ) { - const err = createSafeError(_err); - // This could be a runtime error from Vite's SSR module, so try to fix it here try { loader.fixStacktrace(err); @@ -20,12 +17,11 @@ export function recordServerError( // This is our last line of defense regarding errors where we still might have some information about the request // Our error should already be complete, but let's try to add a bit more through some guesswork - const errorWithMetadata = collectErrorMetadata(err, config.root); + const errorWithMetadata = collectErrorMetadata(err, manifest.rootDir); logger.error(null, formatErrorMessage(errorWithMetadata, logger.level() === 'debug')); return { - error: err, errorWithMetadata, }; } diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index 1b30bc0a3b48..127572cdb065 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -52,11 +52,10 @@ export default function createVitePluginAstroServer({ } const loader = createViteLoader(viteServer); - const entrypoint = settings.adapter?.devEntrypoint ?? 'astro/app/dev'; - const createExports = await loader.import(entrypoint.toString()); + const createExports = await loader.import('astro:app'); const controller = createController({ loader }); - const { handler } = await createExports.default(settings, controller, loader); - + const { handler } = await createExports.default(controller, settings, loader); + const { manifest } = await loader.import('astro:serialized-manifest'); const localStorage = new AsyncLocalStorage(); function handleUnhandledRejection(rejection: any) { @@ -70,7 +69,7 @@ export default function createVitePluginAstroServer({ if (store instanceof IncomingMessage) { setRouteError(controller.state, store.url!, error); } - const { errorWithMetadata } = recordServerError(loader, settings.config, logger, error); + const { errorWithMetadata } = recordServerError(loader, manifest, logger, error); setTimeout( async () => loader.webSocketSend(await getViteErrorPayload(errorWithMetadata)), 200, @@ -202,7 +201,7 @@ export function createDevelopmentManifest(settings: AstroSettings): SSRManifest } return { - hrefRoot: settings.config.root.toString(), + rootDir: settings.config.root, srcDir: settings.config.srcDir, cacheDir: settings.config.cacheDir, outDir: settings.config.outDir, diff --git a/packages/astro/src/vite-plugin-astro-server/server-state.ts b/packages/astro/src/vite-plugin-astro-server/server-state.ts index 5e6c13111dc1..e6cf33071b42 100644 --- a/packages/astro/src/vite-plugin-astro-server/server-state.ts +++ b/packages/astro/src/vite-plugin-astro-server/server-state.ts @@ -18,7 +18,11 @@ export function createServerState(): ServerState { }; } -export function setRouteError(serverState: ServerState, pathname: string, error: Error) { +export function setRouteError( + serverState: ServerState, + pathname: string, + error: Error | undefined, +) { if (serverState.routes.has(pathname)) { const routeState = serverState.routes.get(pathname)!; routeState.state = 'error'; diff --git a/packages/astro/src/vite-plugin-head/index.ts b/packages/astro/src/vite-plugin-head/index.ts index 8b593f16493d..df9fd6388cd5 100644 --- a/packages/astro/src/vite-plugin-head/index.ts +++ b/packages/astro/src/vite-plugin-head/index.ts @@ -63,7 +63,7 @@ export default function configHeadVitePlugin(): vite.Plugin { if (result) { let info = this.getModuleInfo(result.id); const astro = info && getAstroMetadata(info); - if (astro) { + if (astro && isRunnableDevEnvironment(environment)) { if (astro.propagation === 'self' || astro.propagation === 'in-tree') { propagateMetadata.call(this, importer, 'propagation', 'in-tree'); } @@ -77,6 +77,9 @@ export default function configHeadVitePlugin(): vite.Plugin { } }, transform(source, id) { + if (!isRunnableDevEnvironment(environment)) { + return; + } // TODO This could probably be removed now that this is handled in resolveId let info = this.getModuleInfo(id); if (info && getAstroMetadata(info)?.containsHead) { diff --git a/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/dev.ts b/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/dev.ts index 6318f8155fe4..d58702a9918e 100644 --- a/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/dev.ts +++ b/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/dev.ts @@ -1,4 +1,5 @@ import { manifest } from "astro:serialized-manifest"; +import { routes } from "astro:routes" import { createExports } from "./worker"; -export default createExports(manifest).default +export default createExports(manifest, routes).default diff --git a/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/worker.ts b/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/worker.ts index 8293f8a7a228..1b869e4a143e 100644 --- a/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/worker.ts +++ b/packages/integrations/cloudflare/test/fixtures/vite-plugin/src/worker.ts @@ -1,11 +1,15 @@ import type { SSRManifest } from 'astro'; - -import { App } from 'astro/app'; import { handle, type Env } from '@astrojs/cloudflare/handler' import type { ExportedHandler } from '@cloudflare/workers-types'; +import type { RouteInfo } from 'astro'; +import { DevApp, createConsoleLogger } from 'astro/app'; +import type { RoutesList } from 'astro/dist/types/astro.ts'; + -export function createExports(manifest: SSRManifest) { - const app = new App(manifest); +export function createExports(manifest: SSRManifest, routes: RouteInfo[]) { + const routesList: RoutesList = { routes: routes.map((r: RouteInfo) => r.routeData) }; + const logger = createConsoleLogger('debug'); + const app = new DevApp(manifest, true, logger, routesList) return { default: { async fetch(request, env, ctx) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e01b14adf40..e2fd68b3239c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -898,7 +898,7 @@ importers: version: link:../../.. vue: specifier: ^3.5.20 - version: 3.5.20(typescript@5.9.2) + version: 3.5.21(typescript@5.9.2) packages/astro/e2e/fixtures/client-idle-timeout: dependencies: @@ -935,7 +935,7 @@ importers: version: 5.38.6 vue: specifier: ^3.5.20 - version: 3.5.20(typescript@5.9.2) + version: 3.5.21(typescript@5.9.2) devDependencies: '@astrojs/preact': specifier: workspace:* @@ -1038,7 +1038,7 @@ importers: version: link:../../.. sass: specifier: ^1.91.0 - version: 1.91.0 + version: 1.92.1 packages/astro/e2e/fixtures/errors: dependencies: @@ -1071,7 +1071,7 @@ importers: version: 18.3.1(react@18.3.1) sass: specifier: ^1.91.0 - version: 1.91.0 + version: 1.92.1 solid-js: specifier: ^1.9.9 version: 1.9.9 @@ -1080,7 +1080,7 @@ importers: version: 5.38.6 vue: specifier: ^3.5.20 - version: 3.5.20(typescript@5.9.2) + version: 3.5.21(typescript@5.9.2) packages/astro/e2e/fixtures/hmr: devDependencies: @@ -1089,7 +1089,7 @@ importers: version: link:../../.. sass: specifier: ^1.91.0 - version: 1.91.0 + version: 1.92.1 packages/astro/e2e/fixtures/hydration-race: dependencies: @@ -1137,7 +1137,7 @@ importers: version: 5.38.6 vue: specifier: ^3.5.20 - version: 3.5.20(typescript@5.9.2) + version: 3.5.21(typescript@5.9.2) devDependencies: '@astrojs/preact': specifier: workspace:* @@ -1193,7 +1193,7 @@ importers: version: 5.38.6 vue: specifier: ^3.5.20 - version: 3.5.20(typescript@5.9.2) + version: 3.5.21(typescript@5.9.2) devDependencies: '@astrojs/preact': specifier: workspace:* @@ -1233,7 +1233,7 @@ importers: version: 5.38.6 vue: specifier: ^3.5.20 - version: 3.5.20(typescript@5.9.2) + version: 3.5.21(typescript@5.9.2) devDependencies: '@astrojs/preact': specifier: workspace:* @@ -1273,7 +1273,7 @@ importers: version: 5.38.6 vue: specifier: ^3.5.20 - version: 3.5.20(typescript@5.9.2) + version: 3.5.21(typescript@5.9.2) devDependencies: '@astrojs/preact': specifier: workspace:* @@ -1313,7 +1313,7 @@ importers: version: 5.38.6 vue: specifier: ^3.5.20 - version: 3.5.20(typescript@5.9.2) + version: 3.5.21(typescript@5.9.2) devDependencies: '@astrojs/preact': specifier: workspace:* @@ -1353,7 +1353,7 @@ importers: version: 5.38.6 vue: specifier: ^3.5.20 - version: 3.5.20(typescript@5.9.2) + version: 3.5.21(typescript@5.9.2) devDependencies: '@astrojs/preact': specifier: workspace:* @@ -1393,7 +1393,7 @@ importers: version: 5.38.6 vue: specifier: ^3.5.20 - version: 3.5.20(typescript@5.9.2) + version: 3.5.21(typescript@5.9.2) devDependencies: '@astrojs/preact': specifier: workspace:* @@ -1649,7 +1649,7 @@ importers: version: 5.38.6 vue: specifier: ^3.5.20 - version: 3.5.20(typescript@5.9.2) + version: 3.5.21(typescript@5.9.2) packages/astro/e2e/fixtures/vite-virtual-modules: dependencies: @@ -1670,7 +1670,7 @@ importers: version: link:../../.. vue: specifier: ^3.5.20 - version: 3.5.20(typescript@5.9.2) + version: 3.5.21(typescript@5.9.2) packages/astro/performance: devDependencies: @@ -9134,12 +9134,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.41.0': - resolution: {integrity: sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.42.0': resolution: {integrity: sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -9150,12 +9144,6 @@ packages: resolution: {integrity: sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.41.0': - resolution: {integrity: sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.42.0': resolution: {integrity: sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -9169,20 +9157,10 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.41.0': - resolution: {integrity: sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.42.0': resolution: {integrity: sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.41.0': - resolution: {integrity: sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.42.0': resolution: {integrity: sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -9196,10 +9174,6 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.41.0': - resolution: {integrity: sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.42.0': resolution: {integrity: sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -9359,27 +9333,15 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@vue/compiler-core@3.5.20': - resolution: {integrity: sha512-8TWXUyiqFd3GmP4JTX9hbiTFRwYHgVL/vr3cqhr4YQ258+9FADwvj7golk2sWNGHR67QgmCZ8gz80nQcMokhwg==} - '@vue/compiler-core@3.5.21': resolution: {integrity: sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==} - '@vue/compiler-dom@3.5.20': - resolution: {integrity: sha512-whB44M59XKjqUEYOMPYU0ijUV0G+4fdrHVKDe32abNdX/kJe1NUEMqsi4cwzXa9kyM9w5S8WqFsrfo1ogtBZGQ==} - '@vue/compiler-dom@3.5.21': resolution: {integrity: sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==} - '@vue/compiler-sfc@3.5.20': - resolution: {integrity: sha512-SFcxapQc0/feWiSBfkGsa1v4DOrnMAQSYuvDMpEaxbpH5dKbnEM5KobSNSgU+1MbHCl+9ftm7oQWxvwDB6iBfw==} - '@vue/compiler-sfc@3.5.21': resolution: {integrity: sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==} - '@vue/compiler-ssr@3.5.20': - resolution: {integrity: sha512-RSl5XAMc5YFUXpDQi+UQDdVjH9FnEpLDHIALg5J0ITHxkEzJ8uQLlo7CIbjPYqmZtt6w0TsIPbo1izYXwDG7JA==} - '@vue/compiler-ssr@3.5.21': resolution: {integrity: sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==} @@ -9397,29 +9359,15 @@ packages: '@vue/reactivity@3.1.5': resolution: {integrity: sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==} - '@vue/reactivity@3.5.20': - resolution: {integrity: sha512-hS8l8x4cl1fmZpSQX/NXlqWKARqEsNmfkwOIYqtR2F616NGfsLUm0G6FQBK6uDKUCVyi1YOL8Xmt/RkZcd/jYQ==} - '@vue/reactivity@3.5.21': resolution: {integrity: sha512-3ah7sa+Cwr9iiYEERt9JfZKPw4A2UlbY8RbbnH2mGCE8NwHkhmlZt2VsH0oDA3P08X3jJd29ohBDtX+TbD9AsA==} - '@vue/runtime-core@3.5.20': - resolution: {integrity: sha512-vyQRiH5uSZlOa+4I/t4Qw/SsD/gbth0SW2J7oMeVlMFMAmsG1rwDD6ok0VMmjXY3eI0iHNSSOBilEDW98PLRKw==} - '@vue/runtime-core@3.5.21': resolution: {integrity: sha512-+DplQlRS4MXfIf9gfD1BOJpk5RSyGgGXD/R+cumhe8jdjUcq/qlxDawQlSI8hCKupBlvM+3eS1se5xW+SuNAwA==} - '@vue/runtime-dom@3.5.20': - resolution: {integrity: sha512-KBHzPld/Djw3im0CQ7tGCpgRedryIn4CcAl047EhFTCCPT2xFf4e8j6WeKLgEEoqPSl9TYqShc3Q6tpWpz/Xgw==} - '@vue/runtime-dom@3.5.21': resolution: {integrity: sha512-3M2DZsOFwM5qI15wrMmNF5RJe1+ARijt2HM3TbzBbPSuBHOQpoidE+Pa+XEaVN+czbHf81ETRoG1ltztP2em8w==} - '@vue/server-renderer@3.5.20': - resolution: {integrity: sha512-HthAS0lZJDH21HFJBVNTtx+ULcIbJQRpjSVomVjfyPkFSpCwvsPTA+jIzOaUm3Hrqx36ozBHePztQFg6pj5aKg==} - peerDependencies: - vue: 3.5.20 - '@vue/server-renderer@3.5.21': resolution: {integrity: sha512-qr8AqgD3DJPJcGvLcJKQo2tAc8OnXRcfxhOJCPF+fcfn5bBGz7VCcO7t+qETOPxpWK1mgysXvVT/j+xWaHeMWA==} peerDependencies: @@ -9428,9 +9376,6 @@ packages: '@vue/shared@3.1.5': resolution: {integrity: sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==} - '@vue/shared@3.5.20': - resolution: {integrity: sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA==} - '@vue/shared@3.5.21': resolution: {integrity: sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==} @@ -12940,11 +12885,6 @@ packages: sass-formatter@0.7.9: resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==} - sass@1.91.0: - resolution: {integrity: sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==} - engines: {node: '>=14.0.0'} - hasBin: true - sass@1.92.1: resolution: {integrity: sha512-ffmsdbwqb3XeyR8jJR6KelIXARM9bFQe8A6Q3W4Klmwy5Ckd5gz7jgUNHo4UOqutU5Sk1DtKLbpDP0nLCg1xqQ==} engines: {node: '>=14.0.0'} @@ -13987,14 +13927,6 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} - vue@3.5.20: - resolution: {integrity: sha512-2sBz0x/wis5TkF1XZ2vH25zWq3G1bFEPOfkBcx2ikowmphoQsPH6X0V3mmPCXA2K1N/XGTnifVyDQP4GfDDeQw==} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - vue@3.5.21: resolution: {integrity: sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==} peerDependencies: @@ -16901,15 +16833,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.41.0(typescript@5.9.2)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.9.2) - '@typescript-eslint/types': 8.41.0 - debug: 4.4.1 - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/project-service@8.42.0(typescript@5.9.2)': dependencies: '@typescript-eslint/tsconfig-utils': 8.42.0(typescript@5.9.2) @@ -16924,10 +16847,6 @@ snapshots: '@typescript-eslint/types': 8.42.0 '@typescript-eslint/visitor-keys': 8.42.0 - '@typescript-eslint/tsconfig-utils@8.41.0(typescript@5.9.2)': - dependencies: - typescript: 5.9.2 - '@typescript-eslint/tsconfig-utils@8.42.0(typescript@5.9.2)': dependencies: typescript: 5.9.2 @@ -16944,26 +16863,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.41.0': {} - '@typescript-eslint/types@8.42.0': {} - '@typescript-eslint/typescript-estree@8.41.0(typescript@5.9.2)': - dependencies: - '@typescript-eslint/project-service': 8.41.0(typescript@5.9.2) - '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.9.2) - '@typescript-eslint/types': 8.41.0 - '@typescript-eslint/visitor-keys': 8.41.0 - debug: 4.4.1 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/typescript-estree@8.42.0(typescript@5.9.2)': dependencies: '@typescript-eslint/project-service': 8.42.0(typescript@5.9.2) @@ -16991,11 +16892,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.41.0': - dependencies: - '@typescript-eslint/types': 8.41.0 - eslint-visitor-keys: 4.2.1 - '@typescript-eslint/visitor-keys@8.42.0': dependencies: '@typescript-eslint/types': 8.42.0 @@ -17215,7 +17111,7 @@ snapshots: '@babel/types': 7.28.2 '@vue/babel-helper-vue-transform-on': 1.5.0 '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.28.3) - '@vue/shared': 3.5.20 + '@vue/shared': 3.5.21 optionalDependencies: '@babel/core': 7.28.3 transitivePeerDependencies: @@ -17232,14 +17128,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vue/compiler-core@3.5.20': - dependencies: - '@babel/parser': 7.28.3 - '@vue/shared': 3.5.20 - entities: 4.5.0 - estree-walker: 2.0.2 - source-map-js: 1.2.1 - '@vue/compiler-core@3.5.21': dependencies: '@babel/parser': 7.28.3 @@ -17248,28 +17136,11 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.20': - dependencies: - '@vue/compiler-core': 3.5.20 - '@vue/shared': 3.5.20 - '@vue/compiler-dom@3.5.21': dependencies: '@vue/compiler-core': 3.5.21 '@vue/shared': 3.5.21 - '@vue/compiler-sfc@3.5.20': - dependencies: - '@babel/parser': 7.28.3 - '@vue/compiler-core': 3.5.20 - '@vue/compiler-dom': 3.5.20 - '@vue/compiler-ssr': 3.5.20 - '@vue/shared': 3.5.20 - estree-walker: 2.0.2 - magic-string: 0.30.18 - postcss: 8.5.6 - source-map-js: 1.2.1 - '@vue/compiler-sfc@3.5.21': dependencies: '@babel/parser': 7.28.3 @@ -17282,11 +17153,6 @@ snapshots: postcss: 8.5.6 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.20': - dependencies: - '@vue/compiler-dom': 3.5.20 - '@vue/shared': 3.5.20 - '@vue/compiler-ssr@3.5.21': dependencies: '@vue/compiler-dom': 3.5.21 @@ -17322,31 +17188,15 @@ snapshots: dependencies: '@vue/shared': 3.1.5 - '@vue/reactivity@3.5.20': - dependencies: - '@vue/shared': 3.5.20 - '@vue/reactivity@3.5.21': dependencies: '@vue/shared': 3.5.21 - '@vue/runtime-core@3.5.20': - dependencies: - '@vue/reactivity': 3.5.20 - '@vue/shared': 3.5.20 - '@vue/runtime-core@3.5.21': dependencies: '@vue/reactivity': 3.5.21 '@vue/shared': 3.5.21 - '@vue/runtime-dom@3.5.20': - dependencies: - '@vue/reactivity': 3.5.20 - '@vue/runtime-core': 3.5.20 - '@vue/shared': 3.5.20 - csstype: 3.1.3 - '@vue/runtime-dom@3.5.21': dependencies: '@vue/reactivity': 3.5.21 @@ -17354,12 +17204,6 @@ snapshots: '@vue/shared': 3.5.21 csstype: 3.1.3 - '@vue/server-renderer@3.5.20(vue@3.5.20(typescript@5.9.2))': - dependencies: - '@vue/compiler-ssr': 3.5.20 - '@vue/shared': 3.5.20 - vue: 3.5.20(typescript@5.9.2) - '@vue/server-renderer@3.5.21(vue@3.5.21(typescript@5.9.2))': dependencies: '@vue/compiler-ssr': 3.5.21 @@ -17368,8 +17212,6 @@ snapshots: '@vue/shared@3.1.5': {} - '@vue/shared@3.5.20': {} - '@vue/shared@3.5.21': {} '@webcomponents/template-shadowroot@0.2.1': {} @@ -18165,7 +18007,7 @@ snapshots: detective-typescript@14.0.0(typescript@5.9.2): dependencies: - '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.42.0(typescript@5.9.2) ast-module-types: 6.0.1 node-source-walk: 7.0.1 typescript: 5.9.2 @@ -21487,14 +21329,6 @@ snapshots: dependencies: suf-log: 2.5.3 - sass@1.91.0: - dependencies: - chokidar: 4.0.3 - immutable: 5.1.3 - source-map-js: 1.2.1 - optionalDependencies: - '@parcel/watcher': 2.5.1 - sass@1.92.1: dependencies: chokidar: 4.0.3 @@ -22426,7 +22260,7 @@ snapshots: '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.3) '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.3) '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.3) - '@vue/compiler-dom': 3.5.20 + '@vue/compiler-dom': 3.5.21 kolorist: 1.8.0 magic-string: 0.30.18 vite: 6.3.5(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.92.1)(yaml@2.8.1) @@ -22624,16 +22458,6 @@ snapshots: vscode-uri@3.1.0: {} - vue@3.5.20(typescript@5.9.2): - dependencies: - '@vue/compiler-dom': 3.5.20 - '@vue/compiler-sfc': 3.5.20 - '@vue/runtime-dom': 3.5.20 - '@vue/server-renderer': 3.5.20(vue@3.5.20(typescript@5.9.2)) - '@vue/shared': 3.5.20 - optionalDependencies: - typescript: 5.9.2 - vue@3.5.21(typescript@5.9.2): dependencies: '@vue/compiler-dom': 3.5.21