Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
f949aa8
fix(ci): Reinstall deps after having published VS Code (#14996)
Princesseuh Dec 10, 2025
16f3994
fix(svelte): allow client directives (#15004)
antonyfaris Dec 12, 2025
9dd9fca
fix(assets): Fixes missing format option for svgs in the passthrough …
Princesseuh Dec 12, 2025
8cdaa00
[ci] format
Princesseuh Dec 12, 2025
8cab2a4
chore: auto format next (#15009)
florian-lefebvre Dec 12, 2025
3c01790
chore(deps): update github-actions (#15019)
renovate[bot] Dec 15, 2025
0d2adac
fix(deps): update all non-major dependencies (#15020)
renovate[bot] Dec 15, 2025
a178422
Support extending the image API props type (#15014)
delucis Dec 15, 2025
0343993
[ci] release (#14997)
astrobot-houston Dec 16, 2025
87b19b8
fix(content-layer): Try a smarter solution to normalize bare image pa…
Princesseuh Dec 16, 2025
ee0a0fc
[ci] format
Princesseuh Dec 16, 2025
44922de
chore: document core/infra architecture (#14815)
florian-lefebvre Dec 16, 2025
84a09b1
[ci] format
florian-lefebvre Dec 16, 2025
8115752
fix(astro): assets vite build log (#15034)
florian-lefebvre Dec 16, 2025
dd06779
chore(sitemap): migrate to astro:routes:resolved (#15033)
florian-lefebvre Dec 16, 2025
7bec4bd
fix: Remote images: Prevent internal caching from interfering with As…
volpeon Dec 17, 2025
97767c2
[ci] format
florian-lefebvre Dec 17, 2025
4e28db8
Update font utility dependencies to use lighter versions (#15055)
delucis Dec 19, 2025
fddde5f
skip flaky view transitions redirect test (#15060)
matthewp Dec 19, 2025
6450ab0
chore(deps): update actions-cool/issues-helper action to v3.7.5 (#15071)
renovate[bot] Dec 28, 2025
a19140f
ClientRouter: Preserve hash fragment during redirects (#15088)
martrapp Dec 30, 2025
8d569b5
try resurrecting a flaky test (#15089)
martrapp Jan 2, 2026
5fdd541
fix(deps): update astro adapters (#15084)
renovate[bot] Jan 4, 2026
718f5e1
fix(deps): update all non-major dependencies (#15072)
renovate[bot] Jan 5, 2026
1847601
fix(deps): update astro client runtimes (#15085)
renovate[bot] Jan 5, 2026
241bb31
fix: move ts-plugin node_modules to dist (#15083)
fkatsuhiro Jan 5, 2026
4248d3e
[ci] format
Princesseuh Jan 5, 2026
bf18385
Update image-size (#15105)
OliverSpeir Jan 5, 2026
9f9374c
fix: components imports paths (#15107)
florian-lefebvre Jan 5, 2026
9a76961
Tailwind example, README.md: update link (#15099)
deining Jan 5, 2026
b2bcd5a
fix(assets): Use Vite's isFileLoadingAllowed to check if a file can b…
Princesseuh Jan 5, 2026
d419fee
[ci] format
Princesseuh Jan 5, 2026
a012a86
Update prettier extension to new one (#15108)
gameroman Jan 5, 2026
e062101
fix(vscode): Correctly handle TypeScript blocks ending with types (#1…
Princesseuh Jan 5, 2026
4dacd36
[ci] format
Princesseuh Jan 5, 2026
fa9c464
fix(svelte): improve Svelte children prop type checking (#15070)
antonyfaris Jan 5, 2026
24cff1a
chore: Replace fast-glob with tinyglobby in language server (#15057)
fabon-f Jan 5, 2026
7afb48c
view transitions: fix Firefox e2e tests for playwright 1.57 (#15113)
martrapp Jan 6, 2026
30168e9
fix(deps): update astro dependencies (#15103)
renovate[bot] Jan 6, 2026
db009ac
fix: lint vt test (#15114)
florian-lefebvre Jan 6, 2026
cd43a80
[ci] format
florian-lefebvre Jan 6, 2026
b1a64a1
fix(deps): update language tools (#15104)
renovate[bot] Jan 6, 2026
9fc4562
Feature/allow node 24 vercel adapter (#15116)
Raanelom Jan 6, 2026
7090dca
feat(fonts): new font resolver abstraction (#15111)
florian-lefebvre Jan 6, 2026
15c117c
[ci] format
florian-lefebvre Jan 6, 2026
b1e8e32
feat(fonts)!: upgrade unifont and support formats (#15117)
florian-lefebvre Jan 6, 2026
12eb4cd
fix(vue): add HTML attributes to generated TypeScript types (#15016)
rahuld109 Jan 7, 2026
b137946
feat: deduplicate context types (#15122)
florian-lefebvre Jan 7, 2026
5444dac
Merge branch 'main' into chore/merge-main-in-next
florian-lefebvre Jan 7, 2026
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
Prev Previous commit
Next Next commit
fix(assets): Fixes missing format option for svgs in the passthrough …
…service (#14987)

* fix(assets): Fixes missing format option for svgs in the passthrough service

* fix: wtf is going on

* chore: changeset
  • Loading branch information
Princesseuh authored Dec 12, 2025
commit 9dd9fca81e5ed3d0d55e0b1624c6515706963b1f
5 changes: 5 additions & 0 deletions .changeset/odd-windows-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fixes SVGs not working in dev mode when using the passthrough image service
18 changes: 12 additions & 6 deletions packages/astro/src/assets/services/noop.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { baseService, type LocalImageService } from './service.js';
import { baseService, verifyOptions, type LocalImageService } from './service.js';
import { isESMImportedImage } from '../utils/imageKind.js';

// Empty service used for platforms that don't support Sharp / users who don't want transformations.
const noopService: LocalImageService = {
...baseService,
propertiesToHash: ['src'],
async validateOptions(options, imageConfig) {
const newOptions = await (baseService.validateOptions?.(options, imageConfig) ?? options);
delete newOptions.format;

return newOptions;
async validateOptions(options) {
if (isESMImportedImage(options.src) && options.src.format === 'svg') {
options.format = 'svg';
} else {
delete options.format;
}

verifyOptions(options);

return options;
},
async transform(inputBuffer, transformOptions) {
return {
Expand Down
142 changes: 70 additions & 72 deletions packages/astro/src/assets/services/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,69 @@ export type BaseServiceTransform = {

const sortNumeric = (a: number, b: number) => a - b;

export function verifyOptions(options: ImageTransform): void {
// `src` is missing or is `undefined`.
if (!options.src || (!isRemoteImage(options.src) && !isESMImportedImage(options.src))) {
throw new AstroError({
...AstroErrorData.ExpectedImage,
message: AstroErrorData.ExpectedImage.message(
JSON.stringify(options.src),
typeof options.src,
JSON.stringify(options, (_, v) => (v === undefined ? null : v)),
),
});
}

if (!isESMImportedImage(options.src)) {
// User passed an `/@fs/` path or a filesystem path instead of the full image.
if (options.src.startsWith('/@fs/') || (!isRemotePath(options.src) && !options.src.startsWith('/'))) {
throw new AstroError({
...AstroErrorData.LocalImageUsedWrongly,
message: AstroErrorData.LocalImageUsedWrongly.message(options.src),
});
}

// For remote images, width and height are explicitly required as we can't infer them from the file
let missingDimension: 'width' | 'height' | 'both' | undefined;
if (!options.width && !options.height) {
missingDimension = 'both';
} else if (!options.width && options.height) {
missingDimension = 'width';
} else if (options.width && !options.height) {
missingDimension = 'height';
}

if (missingDimension) {
throw new AstroError({
...AstroErrorData.MissingImageDimension,
message: AstroErrorData.MissingImageDimension.message(missingDimension, options.src),
});
}
} else {
if (!VALID_SUPPORTED_FORMATS.includes(options.src.format as any)) {
throw new AstroError({
...AstroErrorData.UnsupportedImageFormat,
message: AstroErrorData.UnsupportedImageFormat.message(
options.src.format,
options.src.src,
VALID_SUPPORTED_FORMATS,
),
});
}

if (options.widths && options.densities) {
throw new AstroError(AstroErrorData.IncompatibleDescriptorOptions);
}

if (
(options.src.format === 'svg' && options.format !== 'svg') ||
(options.src.format !== 'svg' && options.format === 'svg')
) {
throw new AstroError(AstroErrorData.UnsupportedImageConversion);
}
}
}

/**
* Basic local service using the included `_image` endpoint.
* This service intentionally does not implement `transform`.
Expand All @@ -143,90 +206,25 @@ const sortNumeric = (a: number, b: number) => a - b;
* ```
*
* This service adhere to the included services limitations:
* - Remote images are passed as is.
* - Only a limited amount of formats are supported.
* - For remote images, `width` and `height` are always required.
*
*/
export const baseService: Omit<LocalImageService, 'transform'> = {
propertiesToHash: DEFAULT_HASH_PROPS,
validateOptions(options) {
// `src` is missing or is `undefined`.
if (!options.src || (!isRemoteImage(options.src) && !isESMImportedImage(options.src))) {
throw new AstroError({
...AstroErrorData.ExpectedImage,
message: AstroErrorData.ExpectedImage.message(
JSON.stringify(options.src),
typeof options.src,
JSON.stringify(options, (_, v) => (v === undefined ? null : v)),
),
});
// We currently do not support processing SVGs, so whenever the input format is a SVG, force the output to also be one
if (isESMImportedImage(options.src) && options.src.format === 'svg') {
options.format = 'svg';
}

// Run verification-only checks
verifyOptions(options);

if (!isESMImportedImage(options.src)) {
// User passed an `/@fs/` path or a filesystem path instead of the full image.
if (
options.src.startsWith('/@fs/') ||
(!isRemotePath(options.src) && !options.src.startsWith('/'))
) {
throw new AstroError({
...AstroErrorData.LocalImageUsedWrongly,
message: AstroErrorData.LocalImageUsedWrongly.message(options.src),
});
}

// For remote images, width and height are explicitly required as we can't infer them from the file
let missingDimension: 'width' | 'height' | 'both' | undefined;
if (!options.width && !options.height) {
missingDimension = 'both';
} else if (!options.width && options.height) {
missingDimension = 'width';
} else if (options.width && !options.height) {
missingDimension = 'height';
}

if (missingDimension) {
throw new AstroError({
...AstroErrorData.MissingImageDimension,
message: AstroErrorData.MissingImageDimension.message(missingDimension, options.src),
});
}
} else {
if (!VALID_SUPPORTED_FORMATS.includes(options.src.format as any)) {
throw new AstroError({
...AstroErrorData.UnsupportedImageFormat,
message: AstroErrorData.UnsupportedImageFormat.message(
options.src.format,
options.src.src,
VALID_SUPPORTED_FORMATS,
),
});
}

if (options.widths && options.densities) {
throw new AstroError(AstroErrorData.IncompatibleDescriptorOptions);
}

// We currently do not support processing SVGs, so whenever the input format is a SVG, force the output to also be one
if (options.src.format === 'svg') {
options.format = 'svg';
}

if (
(options.src.format === 'svg' && options.format !== 'svg') ||
(options.src.format !== 'svg' && options.format === 'svg')
) {
throw new AstroError(AstroErrorData.UnsupportedImageConversion);
}
}

// If the user didn't specify a format, we'll default to `webp`. It offers the best ratio of compatibility / quality
// In the future, hopefully we can replace this with `avif`, alas, Edge. See https://caniuse.com/avif
// Apply defaults and normalization separate from verification
if (!options.format) {
options.format = DEFAULT_OUTPUT_FORMAT;
}

// Sometimes users will pass number generated from division, which can result in floating point numbers
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) {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
import { Image, Picture } from 'astro:assets';
import myImage from '../assets/penguin1.jpg';
import myLogo from '../assets/astro.svg';
---
<html>
<head></head>
Expand All @@ -11,5 +12,8 @@ import myImage from '../assets/penguin1.jpg';
<div id="picture">
<Picture src={myImage} alt="a penguin" />
</div>
<div id="logo">
<Image src={myLogo} alt="Astro logo" />
</div>
</body>
</html>
56 changes: 51 additions & 5 deletions packages/astro/test/passthrough-image-service.test.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,53 @@
import assert from 'node:assert/strict';
import { before, describe, it } from 'node:test';
import { after, before, describe, it } from 'node:test';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';

describe('passthroughImageService', () => {
/** @type {import('./test-utils.js').Fixture} */
let fixture;

before(async () => {
fixture = await loadFixture({
root: './fixtures/passthrough-image-service/',
});
});

describe('dev', () => {
let $;
let devServer;

before(async () => {
devServer = await fixture.startDevServer();

const html = await fixture.fetch('/').then((res) => res.text());
$ = cheerio.load(html);
});

after(async () => {
await devServer.stop();
});

it('includes img element in dev', () => {
const $img = $('#image img');
assert.equal($img.length, 1);
});

it('serves SVG logo with correct content type', async () => {
const $img = $('#logo img');
const src = $img.attr('src');

const response = await fixture.fetch(src);
const contentType = response.headers.get('content-type');

assert.ok(contentType.includes('image/svg+xml'), `Expected SVG content type, got: ${contentType}`);
});
});

describe('build', () => {
let $;

before(async () => {
fixture = await loadFixture({
root: './fixtures/passthrough-image-service/',
});

await fixture.build();

const html = await fixture.readFile('/index.html');
Expand Down Expand Up @@ -63,5 +96,18 @@ describe('passthroughImageService', () => {
assert.ok(src.endsWith('.jpg'), `Should preserve jpg format, got: ${src}`);
});
});

describe('SVG Logo component', () => {
it('includes img element', () => {
const $img = $('#logo img');
assert.equal($img.length, 1);
});

it('preserves SVG format', () => {
const $img = $('#logo img');
const src = $img.attr('src');
assert.ok(src.endsWith('.svg'), `Should preserve svg format, got: ${src}`);
});
});
});
});
Loading