fix(image): allow usage of image from any directory (#5932)

Currently, @astrojs/image allows *importing* images from srcDir
only. Importing images from outside srcDir fails miserably *in dev
mode* and produces incorrect src.

This happens because `path.relative(fileURLToPath(config.srcDir), id)`
resolves to "../something" and when joined with '/@astroimage' cancels
it out (`join('/@astroimage', '../../something')` => `'/something'`).

Rework /@astroimage URL scheme to be similar to "/@fs/" scheme—always
export absolute path to the target file.
This commit is contained in:
Alexey Shmalko 2023-01-30 22:29:41 +02:00 committed by GitHub
parent ca91976edb
commit b3e65991f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 173 additions and 45 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/image': minor
---
Allow images from outside srcDir

View file

@ -47,8 +47,7 @@
"image-size": "^1.0.2",
"kleur": "^4.1.5",
"magic-string": "^0.27.0",
"mime": "^3.0.0",
"slash": "^4.0.0"
"mime": "^3.0.0"
},
"devDependencies": {
"@types/http-cache-semantics": "^4.0.1",

View file

@ -1,10 +1,9 @@
import type { AstroConfig } from 'astro';
import MagicString from 'magic-string';
import fs from 'node:fs/promises';
import path, { basename, extname, join } from 'node:path';
import { basename, extname, join } from 'node:path';
import { Readable } from 'node:stream';
import { fileURLToPath, pathToFileURL } from 'node:url';
import slash from 'slash';
import type { Plugin, ResolvedConfig } from 'vite';
import type { IntegrationOptions } from './index.js';
import type { InputFormat } from './loaders/index.js';
@ -65,12 +64,7 @@ export function createPlugin(config: AstroConfig, options: Required<IntegrationO
meta.src = `__ASTRO_IMAGE_ASSET__${handle}__`;
} else {
const relId = path.relative(fileURLToPath(config.srcDir), id);
meta.src = join('/@astroimage', relId);
// Windows compat
meta.src = slash(meta.src);
meta.src = '/@astroimage' + url.pathname;
}
return `export default ${JSON.stringify(meta)}`;
@ -78,9 +72,9 @@ export function createPlugin(config: AstroConfig, options: Required<IntegrationO
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
if (req.url?.startsWith('/@astroimage/')) {
const [, id] = req.url.split('/@astroimage/');
// Reconstructing URL to get rid of query parameters in path
const url = new URL(req.url.slice('/@astroimage'.length), 'file:');
const url = new URL(id, config.srcDir);
const file = await fs.readFile(url);
const meta = await metadata(url);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -1,6 +1,7 @@
---
import socialJpg from '../assets/social.jpg';
import introJpg from '../assets/blog/introducing astro.jpg';
import outsideSrc from '../../social.png';
import { Image } from '@astrojs/image/components';
const publicImage = new URL('./hero.jpg', Astro.url);
---
@ -18,6 +19,8 @@ const publicImage = new URL('./hero.jpg', Astro.url);
<br />
<Image id="no-transforms" src={socialJpg} alt="no-transforms" />
<br />
<Image id="outside-src" src={outsideSrc} alt="outside-src" />
<br />
<Image id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" width={544} height={184} format="webp" alt="Google" />
<br />
<Image id="inline" src={import('../assets/social.jpg')} width={506} alt="inline" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View file

@ -1,6 +1,7 @@
---
import socialJpg from '../assets/social.jpg';
import introJpg from '../assets/blog/introducing astro.jpg';
import outsideSrc from '../../social.png';
import { Picture } from '@astrojs/image/components';
const publicImage = new URL('./hero.jpg', Astro.url);
---
@ -14,6 +15,8 @@ const publicImage = new URL('./hero.jpg', Astro.url);
<br />
<Picture id="spaces" src={introJpg} sizes="100vw" widths={[384, 768]} aspectRatio={768/414} alt="spaces" />
<br />
<Picture id="outside-src" src={outsideSrc} sizes="100vw" widths={[384, 768]} aspectRatio={768/414} alt="outside-src" />
<br />
<Picture id="social-jpg" src={socialJpg} sizes="(min-width: 640px) 50vw, 100vw" widths={[253, 506]} alt="Social image" />
<br />
<Picture id="google" src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" sizes="(min-width: 640px) 50vw, 100vw" widths={[272, 544]} aspectRatio={544/184} alt="Google logo" formats={["avif", "webp", "png"]} />

View file

@ -2,9 +2,13 @@ import { expect } from 'chai';
import * as cheerio from 'cheerio';
import sizeOf from 'image-size';
import fs from 'fs/promises';
import { fileURLToPath } from 'url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { join } from 'node:path';
import { loadFixture } from './test-utils.js';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const toAstroImage = (relpath) => '/@astroimage' + pathToFileURL(join(__dirname, 'fixtures/basic-image', relpath)).pathname;
describe('SSG images - dev', function () {
let fixture;
let devServer;
@ -25,25 +29,32 @@ describe('SSG images - dev', function () {
{
title: 'Local images',
id: '#social-jpg',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
},
{
title: 'Local image no transforms',
id: '#no-transforms',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: {},
},
{
title: 'Filename with spaces',
id: '#spaces',
url: '/@astroimage/assets/blog/introducing astro.jpg',
url: toAstroImage('src/assets/blog/introducing astro.jpg'),
query: { f: 'webp', w: '768', h: '414' },
},
{
title: 'File outside src',
id: '#outside-src',
url: toAstroImage('social.png'),
query: { f: 'png', w: '2024', h: '1012' },
contentType: 'image/png',
},
{
title: 'Inline imports',
id: '#inline',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
},
{
@ -123,19 +134,32 @@ describe('SSG images with subpath - dev', function () {
{
title: 'Local images',
id: '#social-jpg',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
},
{
title: 'Local image no transforms',
id: '#no-transforms',
url: toAstroImage('src/assets/social.jpg'),
query: {},
},
{
title: 'Filename with spaces',
id: '#spaces',
url: '/@astroimage/assets/blog/introducing astro.jpg',
url: toAstroImage('src/assets/blog/introducing astro.jpg'),
query: { f: 'webp', w: '768', h: '414' },
},
{
title: 'File outside src',
id: '#outside-src',
url: toAstroImage('social.png'),
query: { f: 'png', w: '2024', h: '1012' },
contentType: 'image/png',
},
{
title: 'Inline imports',
id: '#inline',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
},
{
@ -210,8 +234,7 @@ describe('SSG images - build', function () {
});
function verifyImage(pathname, expected) {
const url = new URL('./fixtures/basic-image/dist/' + pathname, import.meta.url);
const dist = fileURLToPath(url);
const dist = join(fileURLToPath(new URL('.', import.meta.url)), 'fixtures/basic-image/dist', pathname);
const result = sizeOf(dist);
expect(result).to.deep.equal(expected);
}
@ -229,6 +252,12 @@ describe('SSG images - build', function () {
regex: /^\/_astro\/introducing astro.\w{8}_\w{4,10}.webp/,
size: { width: 768, height: 414, type: 'webp' },
},
{
title: 'File outside src',
id: '#outside-src',
regex: /^\/_astro\/social.\w{8}_\w{4,10}.png/,
size: { type: 'png', width: 2024, height: 1012 },
},
{
title: 'Inline imports',
id: '#inline',
@ -311,6 +340,12 @@ describe('SSG images with subpath - build', function () {
regex: /^\/docs\/_astro\/introducing astro.\w{8}_\w{4,10}.webp/,
size: { width: 768, height: 414, type: 'webp' },
},
{
title: 'File outside src',
id: '#outside-src',
regex: /^\/docs\/_astro\/social.\w{8}_\w{4,10}.png/,
size: { type: 'png', width: 2024, height: 1012 },
},
{
title: 'Inline imports',
id: '#inline',

View file

@ -1,8 +1,13 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { join } from 'node:path';
import { loadFixture } from './test-utils.js';
import testAdapter from '../../../astro/test/test-adapter.js';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const toAstroImage = (relpath) => '/@astroimage' + pathToFileURL(join(__dirname, 'fixtures/basic-image', relpath)).pathname;
describe('SSR images - dev', function () {
let fixture;
let devServer;
@ -28,28 +33,35 @@ describe('SSR images - dev', function () {
{
title: 'Local images',
id: '#social-jpg',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
contentType: 'image/jpeg',
},
{
title: 'Local image no transforms',
id: '#no-transforms',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: {},
contentType: 'image/jpeg',
},
{
title: 'Filename with spaces',
id: '#spaces',
url: '/@astroimage/assets/blog/introducing astro.jpg',
url: toAstroImage('src/assets/blog/introducing astro.jpg'),
query: { f: 'webp', w: '768', h: '414' },
contentType: 'image/webp',
},
{
title: 'File outside src',
id: '#outside-src',
url: toAstroImage('social.png'),
query: { f: 'png', w: '2024', h: '1012' },
contentType: 'image/png',
},
{
title: 'Inline imports',
id: '#inline',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
contentType: 'image/jpeg',
},
@ -150,21 +162,28 @@ describe('SSR images with subpath - dev', function () {
{
title: 'Local images',
id: '#social-jpg',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
contentType: 'image/jpeg',
},
{
title: 'Filename with spaces',
id: '#spaces',
url: '/@astroimage/assets/blog/introducing astro.jpg',
url: toAstroImage('src/assets/blog/introducing astro.jpg'),
query: { f: 'webp', w: '768', h: '414' },
contentType: 'image/webp',
},
{
title: 'File outside src',
id: '#outside-src',
url: toAstroImage('social.png'),
query: { f: 'png', w: '2024', h: '1012' },
contentType: 'image/png',
},
{
title: 'Inline imports',
id: '#inline',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
contentType: 'image/jpeg',
},

View file

@ -3,9 +3,13 @@ import * as cheerio from 'cheerio';
import fs from 'fs';
import sizeOf from 'image-size';
import path from 'path';
import { fileURLToPath } from 'url';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { join } from 'node:path';
import { loadFixture } from './test-utils.js';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const toAstroImage = (relpath) => '/@astroimage' + pathToFileURL(join(__dirname, 'fixtures/basic-picture', relpath)).pathname;
describe('SSG pictures - dev', function () {
let fixture;
let devServer;
@ -26,21 +30,28 @@ describe('SSG pictures - dev', function () {
{
title: 'Local images',
id: '#social-jpg',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
alt: 'Social image',
},
{
title: 'Filename with spaces',
id: '#spaces',
url: '/@astroimage/assets/blog/introducing astro.jpg',
url: toAstroImage('src/assets/blog/introducing astro.jpg'),
query: { f: 'jpg', w: '768', h: '414' },
alt: 'spaces',
},
{
title: 'File outside src',
id: '#outside-src',
url: toAstroImage('social.png'),
query: { f: 'png', w: '768', h: '414' },
alt: 'outside-src',
},
{
title: 'Inline imports',
id: '#inline',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
alt: 'Inline social image',
},
@ -120,21 +131,28 @@ describe('SSG pictures with subpath - dev', function () {
{
title: 'Local images',
id: '#social-jpg',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
alt: 'Social image',
},
{
title: 'Filename with spaces',
id: '#spaces',
url: '/@astroimage/assets/blog/introducing astro.jpg',
url: toAstroImage('src/assets/blog/introducing astro.jpg'),
query: { f: 'jpg', w: '768', h: '414' },
alt: 'spaces',
},
{
title: 'File outside src',
id: '#outside-src',
url: toAstroImage('social.png'),
query: { f: 'png', w: '768', h: '414' },
alt: 'outside-src',
},
{
title: 'Inline imports',
id: '#inline',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
alt: 'Inline social image',
},
@ -222,6 +240,13 @@ describe('SSG pictures - build', function () {
size: { width: 768, height: 414, type: 'jpg' },
alt: 'spaces',
},
{
title: 'File outside src',
id: '#outside-src',
regex: /^\/_astro\/social.\w{8}_\w{4,10}.png/,
size: { type: 'png', width: 768, height: 414 },
alt: 'outside-src',
},
{
title: 'Inline images',
id: '#inline',
@ -322,6 +347,13 @@ describe('SSG pictures with subpath - build', function () {
size: { width: 506, height: 253, type: 'jpg' },
alt: 'Social image',
},
{
title: 'File outside src',
id: '#outside-src',
regex: /^\/docs\/_astro\/social.\w{8}_\w{4,10}.png/,
size: { type: 'png', width: 768, height: 414 },
alt: 'outside-src',
},
{
title: 'Inline images',
id: '#inline',

View file

@ -30,6 +30,13 @@ describe('SSR pictures - build', function () {
query: { w: '768', h: '414', f: 'jpg', href: /^\/_astro\/introducing astro.\w{8}.jpg/ },
alt: 'spaces',
},
{
title: 'File outside src',
id: '#outside-src',
url: '/_image',
query: { w: '768', h: '414', f: 'png', href: /^\/_astro\/social.\w{8}.png/ },
alt: 'outside-src',
},
{
title: 'Inline imports',
id: '#inline',
@ -141,6 +148,13 @@ describe('SSR pictures with subpath - build', function () {
query: { w: '768', h: '414', f: 'jpg', href: /^\/docs\/_astro\/introducing astro.\w{8}.jpg/ },
alt: 'spaces',
},
{
title: 'File outside src',
id: '#outside-src',
url: '/_image',
query: { w: '768', h: '414', f: 'png', href: /^\/docs\/_astro\/social.\w{8}.png/ },
alt: 'outside-src',
},
{
title: 'Inline imports',
id: '#inline',

View file

@ -1,8 +1,13 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { join } from 'node:path';
import { loadFixture } from './test-utils.js';
import testAdapter from '../../../astro/test/test-adapter.js';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const toAstroImage = (relpath) => '/@astroimage' + pathToFileURL(join(__dirname, 'fixtures/basic-picture', relpath)).pathname;
describe('SSR pictures - dev', function () {
let fixture;
let devServer;
@ -28,7 +33,7 @@ describe('SSR pictures - dev', function () {
{
title: 'Local images',
id: '#social-jpg',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
contentType: 'image/jpeg',
alt: 'Social image',
@ -36,15 +41,23 @@ describe('SSR pictures - dev', function () {
{
title: 'Filename with spaces',
id: '#spaces',
url: '/@astroimage/assets/blog/introducing astro.jpg',
url: toAstroImage('src/assets/blog/introducing astro.jpg'),
query: { w: '768', h: '414', f: 'jpg' },
contentType: 'image/jpeg',
alt: 'spaces',
},
{
title: 'File outside src',
id: '#outside-src',
url: toAstroImage('social.png'),
query: { f: 'png', w: '768', h: '414' },
contentType: 'image/png',
alt: 'outside-src',
},
{
title: 'Inline imports',
id: '#inline',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
contentType: 'image/jpeg',
alt: 'Inline social image',
@ -157,7 +170,7 @@ describe('SSR pictures with subpath - dev', function () {
{
title: 'Local images',
id: '#social-jpg',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
contentType: 'image/jpeg',
alt: 'Social image',
@ -165,15 +178,23 @@ describe('SSR pictures with subpath - dev', function () {
{
title: 'Filename with spaces',
id: '#spaces',
url: '/@astroimage/assets/blog/introducing astro.jpg',
url: toAstroImage('src/assets/blog/introducing astro.jpg'),
query: { w: '768', h: '414', f: 'jpg' },
contentType: 'image/jpeg',
alt: 'spaces',
},
{
title: 'File outside src',
id: '#outside-src',
url: toAstroImage('social.png'),
query: { f: 'png', w: '768', h: '414' },
contentType: 'image/png',
alt: 'outside-src',
},
{
title: 'Inline imports',
id: '#inline',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
contentType: 'image/jpeg',
alt: 'Inline social image',

View file

@ -1,7 +1,12 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { join } from 'node:path';
import { loadFixture } from './test-utils.js';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const toAstroImage = (relpath) => '/@astroimage' + pathToFileURL(join(__dirname, 'fixtures/squoosh-service', relpath)).pathname;
describe('Squoosh service', function () {
let fixture;
let devServer;
@ -22,7 +27,7 @@ describe('Squoosh service', function () {
{
title: 'Local images',
id: '#social-jpg',
url: '/@astroimage/assets/social.jpg',
url: toAstroImage('src/assets/social.jpg'),
query: { f: 'jpg', w: '506', h: '253' },
},
{

View file

@ -2706,7 +2706,6 @@ importers:
mocha: ^9.2.2
rollup-plugin-copy: ^3.4.0
sharp: ^0.31.0
slash: ^4.0.0
vite: ^4.0.3
dependencies:
'@altano/tiny-async-pool': 1.0.2
@ -2715,7 +2714,6 @@ importers:
kleur: 4.1.5
magic-string: 0.27.0
mime: 3.0.0
slash: 4.0.0
devDependencies:
'@types/http-cache-semantics': 4.0.1
'@types/mime': 2.0.3