summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/gold-trains-eat.md6
-rw-r--r--packages/astro/package.json2
-rw-r--r--packages/astro/src/assets/endpoint/generic.ts (renamed from packages/astro/src/assets/image-endpoint.ts)7
-rw-r--r--packages/astro/src/assets/endpoint/node.ts88
-rw-r--r--packages/astro/src/assets/internal.ts7
-rw-r--r--packages/astro/src/assets/vite-plugin-assets.ts8
-rw-r--r--packages/astro/src/core/build/index.ts2
-rw-r--r--packages/astro/src/core/dev/container.ts2
-rw-r--r--packages/astro/test/core-image.test.js9
-rw-r--r--packages/integrations/node/src/index.ts5
10 files changed, 118 insertions, 18 deletions
diff --git a/.changeset/gold-trains-eat.md b/.changeset/gold-trains-eat.md
new file mode 100644
index 000000000..207c819a7
--- /dev/null
+++ b/.changeset/gold-trains-eat.md
@@ -0,0 +1,6 @@
+---
+'@astrojs/node': patch
+'astro': patch
+---
+
+Use a Node-specific image endpoint to resolve images in dev and Node SSR. This should fix many issues related to getting 404 from the \_image endpoint under certain configurations
diff --git a/packages/astro/package.json b/packages/astro/package.json
index afa6a47d7..4cdadee47 100644
--- a/packages/astro/package.json
+++ b/packages/astro/package.json
@@ -56,7 +56,7 @@
"./components/*": "./components/*",
"./assets": "./dist/assets/index.js",
"./assets/utils": "./dist/assets/utils/index.js",
- "./assets/image-endpoint": "./dist/assets/image-endpoint.js",
+ "./assets/endpoint/*": "./dist/assets/endpoint/*.js",
"./assets/services/sharp": "./dist/assets/services/sharp.js",
"./assets/services/squoosh": "./dist/assets/services/squoosh.js",
"./assets/services/noop": "./dist/assets/services/noop.js",
diff --git a/packages/astro/src/assets/image-endpoint.ts b/packages/astro/src/assets/endpoint/generic.ts
index b7f027536..140189fe0 100644
--- a/packages/astro/src/assets/image-endpoint.ts
+++ b/packages/astro/src/assets/endpoint/generic.ts
@@ -1,8 +1,8 @@
import { isRemotePath } from '@astrojs/internal-helpers/path';
import mime from 'mime/lite.js';
-import type { APIRoute } from '../@types/astro.js';
-import { getConfiguredImageService, isRemoteAllowed } from './internal.js';
-import { etag } from './utils/etag.js';
+import type { APIRoute } from '../../@types/astro.js';
+import { getConfiguredImageService, isRemoteAllowed } from '../internal.js';
+import { etag } from '../utils/etag.js';
// @ts-expect-error
import { imageConfig } from 'astro:assets';
@@ -40,7 +40,6 @@ export const GET: APIRoute = async ({ request }) => {
let inputBuffer: Buffer | undefined = undefined;
- // TODO: handle config subpaths?
const sourceUrl = isRemotePath(transform.src)
? new URL(transform.src)
: new URL(transform.src, url.origin);
diff --git a/packages/astro/src/assets/endpoint/node.ts b/packages/astro/src/assets/endpoint/node.ts
new file mode 100644
index 000000000..1e9616264
--- /dev/null
+++ b/packages/astro/src/assets/endpoint/node.ts
@@ -0,0 +1,88 @@
+import { isRemotePath, removeQueryString } from '@astrojs/internal-helpers/path';
+import { readFile } from 'fs/promises';
+import mime from 'mime/lite.js';
+import type { APIRoute } from '../../@types/astro.js';
+import { getConfiguredImageService, isRemoteAllowed } from '../internal.js';
+import { etag } from '../utils/etag.js';
+// @ts-expect-error
+import { assetsDir, imageConfig } from 'astro:assets';
+
+async function loadLocalImage(src: string, url: URL) {
+ const filePath = import.meta.env.DEV
+ ? removeQueryString(src.slice('/@fs'.length))
+ : new URL('.' + src, assetsDir);
+ let buffer: Buffer | undefined = undefined;
+
+ try {
+ buffer = await readFile(filePath);
+ } catch (e) {
+ const sourceUrl = new URL(src, url.origin);
+ buffer = await loadRemoteImage(sourceUrl);
+ }
+
+ return buffer;
+}
+
+async function loadRemoteImage(src: URL) {
+ try {
+ const res = await fetch(src);
+
+ if (!res.ok) {
+ return undefined;
+ }
+
+ return Buffer.from(await res.arrayBuffer());
+ } catch (err: unknown) {
+ return undefined;
+ }
+}
+
+/**
+ * Endpoint used in dev and SSR to serve optimized images by the base image services
+ */
+export const GET: APIRoute = async ({ request }) => {
+ try {
+ const imageService = await getConfiguredImageService();
+
+ if (!('transform' in imageService)) {
+ throw new Error('Configured image service is not a local service');
+ }
+
+ const url = new URL(request.url);
+ const transform = await imageService.parseURL(url, imageConfig);
+
+ if (!transform?.src) {
+ throw new Error('Incorrect transform returned by `parseURL`');
+ }
+
+ let inputBuffer: Buffer | undefined = undefined;
+
+ if (isRemotePath(transform.src)) {
+ if (isRemoteAllowed(transform.src, imageConfig) === false) {
+ return new Response('Forbidden', { status: 403 });
+ }
+
+ inputBuffer = await loadRemoteImage(new URL(transform.src));
+ } else {
+ inputBuffer = await loadLocalImage(transform.src, url);
+ }
+
+ if (!inputBuffer) {
+ return new Response('Not Found', { status: 404 });
+ }
+
+ const { data, format } = await imageService.transform(inputBuffer, transform, imageConfig);
+
+ return new Response(data, {
+ status: 200,
+ headers: {
+ 'Content-Type': mime.getType(format) ?? `image/${format}`,
+ 'Cache-Control': 'public, max-age=31536000',
+ ETag: etag(data.toString()),
+ Date: new Date().toUTCString(),
+ },
+ });
+ } catch (err: unknown) {
+ return new Response(`Server Error: ${err}`, { status: 500 });
+ }
+};
diff --git a/packages/astro/src/assets/internal.ts b/packages/astro/src/assets/internal.ts
index f6c3b0b52..9cb48f588 100644
--- a/packages/astro/src/assets/internal.ts
+++ b/packages/astro/src/assets/internal.ts
@@ -10,10 +10,11 @@ import type {
} from './types.js';
import { matchHostname, matchPattern } from './utils/remotePattern.js';
-export function injectImageEndpoint(settings: AstroSettings) {
- const endpointEntrypoint = settings.config.image.endpoint ?? 'astro/assets/image-endpoint';
+export function injectImageEndpoint(settings: AstroSettings, mode: 'dev' | 'build') {
+ const endpointEntrypoint =
+ settings.config.image.endpoint ??
+ (mode === 'dev' ? 'astro/assets/endpoint/node' : 'astro/assets/endpoint/generic');
- // TODO: Add a setting to disable the image endpoint
settings.injectedRoutes.push({
pattern: '/_image',
entryPoint: endpointEntrypoint,
diff --git a/packages/astro/src/assets/vite-plugin-assets.ts b/packages/astro/src/assets/vite-plugin-assets.ts
index 9c95b6dc4..fd3ca2c32 100644
--- a/packages/astro/src/assets/vite-plugin-assets.ts
+++ b/packages/astro/src/assets/vite-plugin-assets.ts
@@ -10,6 +10,7 @@ import {
prependForwardSlash,
removeQueryString,
} from '../core/path.js';
+import { isServerLikeOutput } from '../prerender/utils.js';
import { VALID_INPUT_FORMATS, VIRTUAL_MODULE_ID, VIRTUAL_SERVICE_ID } from './consts.js';
import { emitESMImage } from './utils/emitAsset.js';
import { hashTransform, propsToFilename } from './utils/transformToPath.js';
@@ -58,6 +59,13 @@ export default function assets({
export { default as Image } from "astro/components/Image.astro";
export const imageConfig = ${JSON.stringify(settings.config.image)};
+ export const assetsDir = new URL(${JSON.stringify(
+ new URL(
+ isServerLikeOutput(settings.config)
+ ? settings.config.build.client
+ : settings.config.outDir
+ )
+ )});
export const getImage = async (options) => await getImageInternal(options, imageConfig);
`;
}
diff --git a/packages/astro/src/core/build/index.ts b/packages/astro/src/core/build/index.ts
index aefea5080..5f5ae69a1 100644
--- a/packages/astro/src/core/build/index.ts
+++ b/packages/astro/src/core/build/index.ts
@@ -111,7 +111,7 @@ class AstroBuilder {
});
if (isServerLikeOutput(this.settings.config)) {
- this.settings = injectImageEndpoint(this.settings);
+ this.settings = injectImageEndpoint(this.settings, 'build');
}
this.manifest = createRouteManifest({ settings: this.settings }, this.logger);
diff --git a/packages/astro/src/core/dev/container.ts b/packages/astro/src/core/dev/container.ts
index 52dd4c1a4..6cc5713f2 100644
--- a/packages/astro/src/core/dev/container.ts
+++ b/packages/astro/src/core/dev/container.ts
@@ -50,7 +50,7 @@ export async function createContainer({
isRestart,
});
- settings = injectImageEndpoint(settings);
+ settings = injectImageEndpoint(settings, 'dev');
const {
base,
diff --git a/packages/astro/test/core-image.test.js b/packages/astro/test/core-image.test.js
index a6ee4342c..653ad5dfd 100644
--- a/packages/astro/test/core-image.test.js
+++ b/packages/astro/test/core-image.test.js
@@ -180,8 +180,6 @@ describe('astro:image', () => {
let html = await res.text();
$ = cheerio.load(html);
- console.log(html);
-
let $img = $('img');
expect($img).to.have.a.lengthOf(1);
@@ -854,17 +852,14 @@ describe('astro:image', () => {
output: 'server',
adapter: testAdapter(),
image: {
+ endpoint: 'astro/assets/endpoint/node',
service: testImageService(),
},
});
await fixture.build();
});
- // TODO
- // This is not working because the image service does a fetch() on the underlying
- // image and we do not have an HTTP server in these tests. We either need
- // to start one, or find another way to tell the image service how to load these files.
- it.skip('dynamic route images are built at response time', async () => {
+ it('dynamic route images are built at response time sss', async () => {
const app = await fixture.loadTestAdapterApp();
let request = new Request('http://example.com/');
let response = await app.render(request);
diff --git a/packages/integrations/node/src/index.ts b/packages/integrations/node/src/index.ts
index 5978371e4..1f3707949 100644
--- a/packages/integrations/node/src/index.ts
+++ b/packages/integrations/node/src/index.ts
@@ -30,8 +30,11 @@ export default function createIntegration(userOptions: UserOptions): AstroIntegr
return {
name: '@astrojs/node',
hooks: {
- 'astro:config:setup': ({ updateConfig }) => {
+ 'astro:config:setup': ({ updateConfig, config }) => {
updateConfig({
+ image: {
+ endpoint: config.image.endpoint ?? 'astro/assets/endpoint/node',
+ },
vite: {
ssr: {
noExternal: ['@astrojs/node'],