Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/cache-disabled-no-throw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'astro': patch
---

Improves the experience of working with experimental route caching in dev mode by replacing some errors with silent no-ops, avoiding the need to write conditional logic to handle different modes

Adds a `cache.enabled` property to `CacheLike` so libraries can check whether caching is active without try/catch.
5 changes: 5 additions & 0 deletions .changeset/fix-resolved-pathname-race.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"astro": patch
---

Fixes a race condition where concurrent requests to dynamic routes in the dev server could produce incorrect params.
5 changes: 5 additions & 0 deletions .changeset/frank-buttons-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes `fit` defaults not being applied unless `layout` was also specified
5 changes: 5 additions & 0 deletions .changeset/funky-knives-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/netlify': patch
---

Fixes the image CDN being used in development despite being disabled in certain cases
5 changes: 5 additions & 0 deletions .changeset/green-eyes-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/netlify': minor
---

Adds support for the `fit` option to the image service
6 changes: 6 additions & 0 deletions .changeset/hip-wings-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'astro': patch
---

Fixes integration-injected scripts (e.g. Alpine.js via `injectScript()`) not being loaded in the dev server when using non-runnable environment adapters like `@astrojs/cloudflare`.

5 changes: 5 additions & 0 deletions .changeset/perky-dots-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/internal-helpers': patch
---

Fixes glob matching of remote patterns matching more paths than intended in select situations
2 changes: 1 addition & 1 deletion .changeset/pre.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"mode": "pre",
"mode": "exit",
"tag": "beta",
"initialVersions": {
"astro": "5.13.7",
Expand Down
5 changes: 5 additions & 0 deletions .changeset/twenty-zebras-hammer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/cloudflare': patch
---

Fixes a regression where using the adapter would throw an error when using an integration that uses JSX.
4 changes: 3 additions & 1 deletion packages/astro/components/Image.astro
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ if (typeof props.height === 'string') {
const layout = props.layout ?? imageConfig.layout ?? 'none';

if (layout !== 'none') {
// Apply defaults from imageConfig if not provided
props.layout ??= imageConfig.layout;
props.fit ??= imageConfig.objectFit ?? 'cover';
props.position ??= imageConfig.objectPosition ?? 'center';
} else if (imageConfig.objectFit || imageConfig.objectPosition) {
props.fit ??= imageConfig.objectFit;
props.position ??= imageConfig.objectPosition;
}

const image = await getImage(props as UnresolvedImageTransform);
Expand Down
4 changes: 3 additions & 1 deletion packages/astro/components/Picture.astro
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,12 @@ const layout = props.layout ?? imageConfig.layout ?? 'none';
const useResponsive = layout !== 'none';

if (useResponsive) {
// Apply defaults from imageConfig if not provided
props.layout ??= imageConfig.layout;
props.fit ??= imageConfig.objectFit ?? 'cover';
props.position ??= imageConfig.objectPosition ?? 'center';
} else if (imageConfig.objectFit || imageConfig.objectPosition) {
props.fit ??= imageConfig.objectFit;
props.position ??= imageConfig.objectPosition;
}

for (const key in props) {
Expand Down
3 changes: 1 addition & 2 deletions packages/astro/src/assets/services/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,7 @@ export const baseService: Omit<LocalImageService, 'transform'> = {
}
if (options.width) options.width = Math.round(options.width);
if (options.height) options.height = Math.round(options.height);
if (options.layout && options.width && options.height) {
options.fit ??= 'cover';
if (options.layout) {
delete options.layout;
}
if (options.fit === 'none') {
Expand Down
4 changes: 0 additions & 4 deletions packages/astro/src/assets/services/sharp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,6 @@ const sharpService: LocalImageService<SharpImageServiceConfig> = {
// get some information about the input
const { format } = await result.metadata();

// If `fit` isn't set then use old behavior:
// - Do not use both width and height for resizing, and prioritize width over height
// - Allow enlarging images

if (transform.width && transform.height) {
const fit: keyof FitEnum | undefined = transform.fit
? (fitMap[transform.fit] ?? 'inside')
Expand Down
52 changes: 24 additions & 28 deletions packages/astro/src/assets/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,30 @@ type ImageSharedProps<T> = T & {
* ```
*/
priority?: boolean;

/**
* Defines how the image should be cropped if the aspect ratio is changed.
*
* Default is `cover`. Allowed values are `fill`, `contain`, `cover`, `none` or `scale-down`. These behave like the equivalent CSS `object-fit` values. Other values may be passed if supported by the image service.
*
* **Example**:
* ```astro
* <Image src={...} fit="contain" alt="..." />
* ```
*/
fit?: ImageFit;

/**
* Defines the position of the image when cropping.
*
* The value is a string that specifies the position of the image, which matches the CSS `object-position` property. Other values may be passed if supported by the image service.
*
* **Example**:
* ```astro
* <Image src={...} position="center top" alt="..." />
* ```
*/
position?: string;
} & (
| {
/**
Expand All @@ -186,32 +210,6 @@ type ImageSharedProps<T> = T & {

layout?: ImageLayout;

/**
* Defines how the image should be cropped if the aspect ratio is changed. Requires `layout` to be set.
*
* Default is `cover`. Allowed values are `fill`, `contain`, `cover`, `none` or `scale-down`. These behave like the equivalent CSS `object-fit` values. Other values may be passed if supported by the image service.
*
* **Example**:
* ```astro
* <Image src={...} fit="contain" alt="..." />
* ```
*/

fit?: ImageFit;

/**
* Defines the position of the image when cropping. Requires `layout` to be set.
*
* The value is a string that specifies the position of the image, which matches the CSS `object-position` property. Other values may be passed if supported by the image service.
*
* **Example**:
* ```astro
* <Image src={...} position="center top" alt="..." />
* ```
*/

position?: string;

/**
* A list of widths to generate images for. The value of this property will be used to assign the `srcset` property on the final `img` element.
*
Expand All @@ -229,8 +227,6 @@ type ImageSharedProps<T> = T & {
densities?: (number | `${number}x`)[];
widths?: never;
layout?: never;
fit?: never;
position?: never;
}
) &
Astro.CustomImageProps;
Expand Down
7 changes: 6 additions & 1 deletion packages/astro/src/core/app/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,12 @@ export abstract class BaseApp<P extends Pipeline = AppPipeline> {
status: 404,
});
}
const pathname = this.getPathnameFromRequest(request);
let pathname = this.getPathnameFromRequest(request);
// In dev, the route may have matched a normalized pathname (after .html stripping).
// Apply the same normalization for correct param extraction.
if (this.isDev()) {
pathname = pathname.replace(/\/index\.html$/, '/').replace(/\.html$/, '');
}
const defaultStatus = this.getDefaultStatusCode(routeData, pathname);

let response;
Expand Down
10 changes: 0 additions & 10 deletions packages/astro/src/core/app/dev/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { RouteData } from '../../../types/public/index.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 type { CreateRenderContext, RenderContext } from '../../render-context.js';
import {
BaseApp,
type DevMatch,
Expand All @@ -20,7 +19,6 @@ import { req } from '../../messages/runtime.js';

export class DevApp extends BaseApp<NonRunnablePipeline> {
logger: Logger;
resolvedPathname: string | undefined = undefined;
constructor(manifest: SSRManifest, streaming = true, logger: Logger) {
super(manifest, streaming, logger);
this.logger = logger;
Expand Down Expand Up @@ -60,20 +58,12 @@ export class DevApp extends BaseApp<NonRunnablePipeline> {
);
if (!matchedRoute) return undefined;

this.resolvedPathname = matchedRoute.resolvedPathname;
return {
routeData: matchedRoute.route,
resolvedPathname: matchedRoute.resolvedPathname,
};
}

async createRenderContext(payload: CreateRenderContext): Promise<RenderContext> {
return super.createRenderContext({
...payload,
pathname: this.resolvedPathname ?? payload.pathname,
});
}

async renderError(
request: Request,
{
Expand Down
7 changes: 7 additions & 0 deletions packages/astro/src/core/cache/runtime/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ const APPLY_HEADERS = Symbol.for('astro:cache:apply');
const IS_ACTIVE = Symbol.for('astro:cache:active');

export interface CacheLike {
/**
* Whether caching is enabled. `false` when no cache provider is configured
* or in dev mode. Libraries can check this before calling cache methods.
*/
readonly enabled: boolean;
/**
* Set cache options for the current request. Call multiple times to merge options.
* Pass `false` to explicitly opt out of caching.
Expand All @@ -34,6 +39,8 @@ export class AstroCache implements CacheLike {
#disabled = false;
#provider: CacheProvider | null;

readonly enabled = true;

constructor(provider: CacheProvider | null) {
this.#provider = provider;
}
Expand Down
36 changes: 30 additions & 6 deletions packages/astro/src/core/cache/runtime/noop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AstroError } from '../../errors/errors.js';
import { CacheNotEnabled } from '../../errors/errors-data.js';
import type { CacheLike } from './cache.js';
import type { CacheOptions } from '../types.js';
import type { Logger } from '../../logger/core.js';

/**
* A no-op cache implementation used in dev mode when cache is configured.
Expand All @@ -11,6 +12,8 @@ import type { CacheOptions } from '../types.js';
const EMPTY_OPTIONS = Object.freeze({ tags: [] }) as Readonly<CacheOptions>;

export class NoopAstroCache implements CacheLike {
readonly enabled = false;

set(): void {}

get tags(): string[] {
Expand All @@ -24,22 +27,43 @@ export class NoopAstroCache implements CacheLike {
async invalidate(): Promise<void> {}
}

let hasWarned = false;

/**
* A cache implementation that throws on any method call.
* Used when cache is not configured — provides a clear, actionable error
* instead of silently doing nothing or returning undefined.
* A no-op cache used when cache is not configured.
* Logs a warning on first use instead of throwing, so libraries
* can call cache methods without needing try/catch.
* `invalidate()` still throws since it implies the caller
* expects purging to actually work.
*/
export class DisabledAstroCache implements CacheLike {
readonly enabled = false;
#logger: Logger | undefined;

constructor(logger?: Logger) {
this.#logger = logger;
}

#warn(): void {
if (!hasWarned) {
hasWarned = true;
this.#logger?.warn(
'cache',
'`cache.set()` was called but caching is not enabled. Configure a cache provider in your Astro config under `experimental.cache` to enable caching.',
);
}
}

set(): void {
throw new AstroError(CacheNotEnabled);
this.#warn();
}

get tags(): string[] {
throw new AstroError(CacheNotEnabled);
return [];
}

get options(): Readonly<CacheOptions> {
throw new AstroError(CacheNotEnabled);
return EMPTY_OPTIONS;
}

async invalidate(): Promise<void> {
Expand Down
4 changes: 2 additions & 2 deletions packages/astro/src/core/render-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ export class RenderContext {
// Create cache instance
let cache: CacheLike;
if (!pipeline.cacheConfig) {
// Cache not configured — throws on use
cache = new DisabledAstroCache();
// Cache not configured — no-ops with a one-time warning
cache = new DisabledAstroCache(pipeline.logger);
} else if (pipeline.runtimeMode === 'development') {
cache = new NoopAstroCache();
} else {
Expand Down
7 changes: 1 addition & 6 deletions packages/astro/src/vite-plugin-app/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ export class AstroServerApp extends BaseApp<RunnablePipeline> {
loader: ModuleLoader;
manifestData: RoutesList;
currentRenderContext: RenderContext | undefined = undefined;
resolvedPathname: string | undefined = undefined;
constructor(
manifest: SSRManifest,
streaming = true,
Expand Down Expand Up @@ -119,10 +118,7 @@ export class AstroServerApp extends BaseApp<RunnablePipeline> {
}

async createRenderContext(payload: CreateRenderContext): Promise<RenderContext> {
this.currentRenderContext = await super.createRenderContext({
...payload,
pathname: this.resolvedPathname ?? payload.pathname,
});
this.currentRenderContext = await super.createRenderContext(payload);
return this.currentRenderContext;
}

Expand Down Expand Up @@ -184,7 +180,6 @@ export class AstroServerApp extends BaseApp<RunnablePipeline> {
throw new Error('No route matched, and default 404 route was not found.');
}

self.resolvedPathname = matchedRoute.resolvedPathname;
const request = createRequest({
url,
headers: incomingRequest.headers,
Expand Down
Loading
Loading