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
5 changes: 5 additions & 0 deletions .changeset/bright-guests-wear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/vercel': patch
---

Improves error message when using Node builtins inside Edge middlewares
6 changes: 6 additions & 0 deletions .changeset/shaggy-aliens-win.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@astrojs/node': patch
'astro': patch
---

fix: fix image 500 error when moving dist directory in standalone Node
28 changes: 24 additions & 4 deletions packages/astro/src/assets/endpoint/node.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
// @ts-expect-error
import { outDir } from 'astro:assets';
import { outDir, serverDir } from 'astro:assets';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { isParentDirectory } from '@astrojs/internal-helpers/path';
import type { APIRoute } from '../../types/public/common.js';
import { handleImageRequest } from './shared.js';

async function loadLocalImage(src: string, url: URL) {
const outDirURL = resolveOutDir();
// If the _image segment isn't at the start of the path, we have a base
const idx = url.pathname.indexOf('/_image');
if (idx > 0) {
// Remove the base path
src = src.slice(idx);
}
if (!URL.canParse('.' + src, outDir)) {
if (!URL.canParse('.' + src, outDirURL)) {
return undefined;
}
const fileUrl = new URL('.' + src, outDir);
const fileUrl = new URL('.' + src, outDirURL);
if (fileUrl.protocol !== 'file:') {
return undefined;
}
if (!isParentDirectory(fileURLToPath(outDir), fileURLToPath(fileUrl))) {
if (!isParentDirectory(fileURLToPath(outDirURL), fileURLToPath(fileUrl))) {
return undefined;
}

Expand All @@ -44,3 +46,21 @@ export const GET: APIRoute = async ({ request }) => {
});
}
};

function resolveOutDir() {
const serverDirPath = fileURLToPath(serverDir);
const rel = path.relative(serverDirPath, fileURLToPath(outDir));

const serverFolder = path.basename(serverDirPath);
let serverEntryFolderURL = path.dirname(import.meta.url);
while (!serverEntryFolderURL.endsWith(serverFolder)) {
serverEntryFolderURL = path.dirname(serverEntryFolderURL);
}
const serverEntryURL = serverEntryFolderURL + '/entry.mjs';
const outDirURL = new URL(appendForwardSlash(rel), serverEntryURL);
return outDirURL;
}

function appendForwardSlash(pth: string) {
return pth.endsWith('/') ? pth : pth + '/';
}
13 changes: 4 additions & 9 deletions packages/astro/src/assets/services/sharp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ const sharpService: LocalImageService<SharpImageServiceConfig> = {
// always call rotate to adjust for EXIF data orientation
result.rotate();

// 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
Expand Down Expand Up @@ -108,15 +111,7 @@ const sharpService: LocalImageService<SharpImageServiceConfig> = {
}
}

const isGifInput =
inputBuffer[0] === 0x47 && // 'G'
inputBuffer[1] === 0x49 && // 'I'
inputBuffer[2] === 0x46 && // 'F'
inputBuffer[3] === 0x38 && // '8'
(inputBuffer[4] === 0x39 || inputBuffer[4] === 0x37) && // '9' or '7'
inputBuffer[5] === 0x61; // 'a'

if (transform.format === 'webp' && isGifInput) {
if (transform.format === 'webp' && format === 'gif') {
// Convert animated GIF to animated WebP with loop=0 (infinite)
result.webp({ quality: typeof quality === 'number' ? quality : undefined, loop: 0 });
} else {
Expand Down
6 changes: 3 additions & 3 deletions packages/astro/src/assets/vite-plugin-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,9 @@ export default function assets({ fs, settings, sync, logger }: Options): vite.Pl
: settings.config.outDir,
),
)});
export const assetsDir = /* #__PURE__ */ new URL(${JSON.stringify(
settings.config.build.assets,
)}, outDir);
export const serverDir = /* #__PURE__ */ new URL(${JSON.stringify(
new URL(settings.config.build.server),
)});
export const getImage = async (options) => await getImageInternal(options, imageConfig);

export const getFontData = createGetFontData(fontsMod);
Expand Down
29 changes: 29 additions & 0 deletions packages/integrations/node/test/image.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as assert from 'node:assert/strict';
import { cp, rm } from 'node:fs/promises';
import { after, before, describe, it } from 'node:test';
import { inferRemoteSize } from 'astro/assets/utils/inferRemoteSize.js';
import * as cheerio from 'cheerio';
Expand Down Expand Up @@ -81,4 +82,32 @@ describe('Image endpoint', () => {
assert.equal(res.status, 403, `Failed on href: ${href}`);
}
});

describe('the dist folder is moved', () => {
const outputDir = new URL('./fixtures/image/output/', import.meta.url);

before(async () => {
await devPreview.stop();
await cp(fixture.config.outDir, new URL('./dist', outputDir), { recursive: true });
await rm(fixture.config.outDir, { recursive: true });
devPreview = await fixture.preview({ outDir: './output/dist' });
});

after(async () => {
await rm(outputDir, { recursive: true });
});

it('it returns local images', async () => {
const res = await fixture.fetch('/');
assert.equal(res.status, 200);
const html = await res.text();
const $ = cheerio.load(html);

const img = $('img[alt=Penguins]').attr('src');
const size = await inferRemoteSize(`http://localhost:4321${img}`);
assert.equal(size.format, 'webp');
assert.equal(size.width, 50);
assert.equal(size.height, 33);
});
});
});
85 changes: 46 additions & 39 deletions packages/integrations/vercel/src/serverless/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ import {
NODE_PATH,
} from '../index.js';

const NODE_BUILTINS_FILTER = new RegExp(
builtinModules.map((mod) => `(^${mod}$|^node:${mod}$)`).join('|'),
);

/**
* It generates the Vercel Edge Middleware file.
*
Expand Down Expand Up @@ -45,42 +41,53 @@ export async function generateEdgeMiddleware(
// https://vercel.com/docs/concepts/functions/edge-middleware#create-edge-middleware
const bundledFilePath = fileURLToPath(outPath);
const esbuild = await import('esbuild');
await esbuild.build({
stdin: {
contents: code,
resolveDir: fileURLToPath(root),
},
// Vercel Edge runtime targets ESNext, because Cloudflare Workers update v8 weekly
// https://github.com/vercel/vercel/blob/1006f2ae9d67ea4b3cbb1073e79d14d063d42436/packages/next/scripts/build-edge-function-template.js
target: 'esnext',
platform: 'browser',
// esbuild automatically adds the browser, import and default conditions
// https://esbuild.github.io/api/#conditions
// https://runtime-keys.proposal.wintercg.org/#edge-light
conditions: ['edge-light', 'workerd', 'worker'],
outfile: bundledFilePath,
allowOverwrite: true,
format: 'esm',
bundle: true,
minify: false,
// ensure node built-in modules are namespaced with `node:`
plugins: [
{
name: 'esbuild-namespace-node-built-in-modules',
setup(build) {
build.onResolve(
{
filter: NODE_BUILTINS_FILTER,
},
(args) => ({
path: 'node:' + args.path,
external: true,
}),
);
},
try {
await esbuild.build({
stdin: {
contents: code,
resolveDir: fileURLToPath(root),
},
],
});
// Vercel Edge runtime targets ESNext, because Cloudflare Workers update v8 weekly
// https://github.com/vercel/vercel/blob/1006f2ae9d67ea4b3cbb1073e79d14d063d42436/packages/next/scripts/build-edge-function-template.js
target: 'esnext',
platform: 'browser',
// esbuild automatically adds the browser, import and default conditions
// https://esbuild.github.io/api/#conditions
// https://runtime-keys.proposal.wintercg.org/#edge-light
conditions: ['edge-light', 'workerd', 'worker'],
outfile: bundledFilePath,
allowOverwrite: true,
format: 'esm',
bundle: true,
minify: false,
// ensure node built-in modules are namespaced with `node:`
plugins: [
{
name: 'esbuild-namespace-node-built-in-modules',
setup(build) {
const filter = new RegExp(builtinModules.map((mod) => `(^${mod}$)`).join('|'));
build.onResolve(
{
filter,
},
(args) => ({
path: 'node:' + args.path,
external: true,
}),
);
},
},
],
});
} catch (err) {
if ((err as Error).message.includes('Could not resolve "node:')) {
logger.error(
`Vercel does not allow the use of Node.js built-ins in edge functions. Please ensure your middleware code and 3rd-party packages don’t use Node built-ins.`,
);
}

throw err;
}
return pathToFileURL(bundledFilePath);
}

Expand Down
Loading