From bbf3f190a574852b8122a88788de8a1ff0d26cad Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Mon, 18 Jul 2022 09:22:13 -0500 Subject: [PATCH 1/5] WIP: always use the built-in sharp service for local images in `dev` --- packages/integrations/image/README.md | 2 ++ packages/integrations/image/package.json | 1 + .../integrations/image/src/endpoints/dev.ts | 4 ++-- packages/integrations/image/src/get-image.ts | 22 ++++++++++++------- packages/integrations/image/src/index.ts | 12 +++++++--- 5 files changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/integrations/image/README.md b/packages/integrations/image/README.md index b14587d71227..ee9fdf3c92a5 100644 --- a/packages/integrations/image/README.md +++ b/packages/integrations/image/README.md @@ -73,6 +73,8 @@ The included `sharp` transformer supports resizing images and encoding them to d The intergration can be configured to run with a different image service, either a hosted image service or a full image transformer that runs locally in your build or SSR deployment. +> During development, local images may not have been published yet and would not be available to hosted image services. Local images will always use the built-in `sharp` service when using `astro dev`. + There are currently no other configuration options for the `@astrojs/image` integration. Please [open an issue](https://github.com/withastro/astro/issues/new/choose) if you have a compelling use case to share.
diff --git a/packages/integrations/image/package.json b/packages/integrations/image/package.json index a2b5486141ad..0fa2291937f1 100644 --- a/packages/integrations/image/package.json +++ b/packages/integrations/image/package.json @@ -25,6 +25,7 @@ "import": "./dist/index.js" }, "./sharp": "./dist/loaders/sharp.js", + "./cloudinary": "./dist/loaders/cloudinary.js", "./endpoints/dev": "./dist/endpoints/dev.js", "./endpoints/prod": "./dist/endpoints/prod.js", "./components": "./components/index.js", diff --git a/packages/integrations/image/src/endpoints/dev.ts b/packages/integrations/image/src/endpoints/dev.ts index 3d8b28993bd4..fb88e9093b87 100644 --- a/packages/integrations/image/src/endpoints/dev.ts +++ b/packages/integrations/image/src/endpoints/dev.ts @@ -1,10 +1,10 @@ import type { APIRoute } from 'astro'; import { lookup } from 'mrmime'; -// @ts-ignore -import loader from 'virtual:image-loader'; import { loadImage } from '../utils.js'; export const get: APIRoute = async ({ request }) => { + const loader = (globalThis as any)['@astrojs/image'].ssrLoader; + try { const url = new URL(request.url); const transform = loader.parseTransform(url.searchParams); diff --git a/packages/integrations/image/src/get-image.ts b/packages/integrations/image/src/get-image.ts index 5eb41cf736eb..467c09278c01 100644 --- a/packages/integrations/image/src/get-image.ts +++ b/packages/integrations/image/src/get-image.ts @@ -8,7 +8,7 @@ import { OutputFormat, TransformOptions, } from './types.js'; -import { parseAspectRatio } from './utils.js'; +import { isRemoteImage, parseAspectRatio } from './utils.js'; export interface GetImageTransform extends Omit { src: string | ImageMetadata | Promise<{ default: ImageMetadata }>; @@ -105,23 +105,29 @@ export async function getImage( loader: ImageService, transform: GetImageTransform ): Promise { - (globalThis as any).loader = loader; + (globalThis as any)['@astrojs/image'].loader = loader; const resolved = await resolveTransform(transform); + const attributes = await loader.getImageAttributes(resolved); + const isDev = (globalThis as any)['@astrojs/image'].command === 'dev'; + const isLocalImage = !isRemoteImage(resolved.src); + + const _loader = isDev && isLocalImage ? (globalThis as any)['@astrojs/image'].ssrLoader : loader; + // For SSR services, build URLs for the injected route - if (isSSRService(loader)) { - const { searchParams } = loader.serializeTransform(resolved); + if (isSSRService(_loader)) { + const { searchParams } = _loader.serializeTransform(resolved); // cache all images rendered to HTML - if (globalThis && (globalThis as any).addStaticImage) { - (globalThis as any)?.addStaticImage(resolved); + if (globalThis && (globalThis as any)['@astrojs/image'].addStaticImage) { + (globalThis as any)['@astrojs/image'].addStaticImage(resolved); } const src = - globalThis && (globalThis as any).filenameFormat - ? (globalThis as any).filenameFormat(resolved, searchParams) + globalThis && (globalThis as any)['@astrojs/image'].filenameFormat + ? (globalThis as any)['@astrojs/image'].filenameFormat(resolved, searchParams) : `${ROUTE_PATTERN}?${searchParams.toString()}`; return { diff --git a/packages/integrations/image/src/index.ts b/packages/integrations/image/src/index.ts index 3721f9667e28..ee4147fc9120 100644 --- a/packages/integrations/image/src/index.ts +++ b/packages/integrations/image/src/index.ts @@ -3,6 +3,7 @@ import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { OUTPUT_DIR, PKG_NAME, ROUTE_PATTERN } from './constants.js'; +import sharp from './loaders/sharp.js'; import { IntegrationOptions, TransformOptions } from './types.js'; import { ensureDir, @@ -16,6 +17,10 @@ export * from './get-image.js'; export * from './get-picture.js'; const createIntegration = (options: IntegrationOptions = {}): AstroIntegration => { + (globalThis as any)['@astrojs/image'] = { + ssrLoader: sharp + }; + const resolvedOptions = { serviceEntryPoint: '@astrojs/image/sharp', ...options, @@ -43,6 +48,7 @@ const createIntegration = (options: IntegrationOptions = {}): AstroIntegration = hooks: { 'astro:config:setup': ({ command, config, injectRoute, updateConfig }) => { _config = config; + (globalThis as any)['@astrojs/image'].command = command; // Always treat `astro dev` as SSR mode, even without an adapter const mode = command === 'dev' || config.adapter ? 'ssr' : 'ssg'; @@ -51,12 +57,12 @@ const createIntegration = (options: IntegrationOptions = {}): AstroIntegration = // Used to cache all images rendered to HTML // Added to globalThis to share the same map in Node and Vite - (globalThis as any).addStaticImage = (transform: TransformOptions) => { + (globalThis as any)['@astrojs/image'].addStaticImage = (transform: TransformOptions) => { staticImages.set(propsToFilename(transform), transform); }; // TODO: Add support for custom, user-provided filename format functions - (globalThis as any).filenameFormat = ( + (globalThis as any)['@astrojs/image'].filenameFormat = ( transform: TransformOptions, searchParams: URLSearchParams ) => { @@ -83,7 +89,7 @@ const createIntegration = (options: IntegrationOptions = {}): AstroIntegration = }, 'astro:build:done': async ({ dir }) => { for await (const [filename, transform] of staticImages) { - const loader = (globalThis as any).loader; + const loader = (globalThis as any)['@astrojs/image'].loader; let inputBuffer: Buffer | undefined = undefined; let outputFile: string; From dcf56e43e49cacc77b0567bc7201c3bf7b19e6c5 Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Mon, 18 Jul 2022 09:54:56 -0500 Subject: [PATCH 2/5] adding type definitions for the integration's use of globalThis --- .../integrations/image/src/endpoints/dev.ts | 6 +++++- packages/integrations/image/src/get-image.ts | 18 +++++++++++------- packages/integrations/image/src/index.ts | 15 ++++++++++----- packages/integrations/image/src/types.ts | 12 ++++++++++++ 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/packages/integrations/image/src/endpoints/dev.ts b/packages/integrations/image/src/endpoints/dev.ts index fb88e9093b87..cff8540b9ac4 100644 --- a/packages/integrations/image/src/endpoints/dev.ts +++ b/packages/integrations/image/src/endpoints/dev.ts @@ -3,7 +3,11 @@ import { lookup } from 'mrmime'; import { loadImage } from '../utils.js'; export const get: APIRoute = async ({ request }) => { - const loader = (globalThis as any)['@astrojs/image'].ssrLoader; + const loader = globalThis.astroImage.ssrLoader; + + if (!loader) { + throw new Error('@astrojs/image: loader not found!'); + } try { const url = new URL(request.url); diff --git a/packages/integrations/image/src/get-image.ts b/packages/integrations/image/src/get-image.ts index 467c09278c01..49c39a4deb45 100644 --- a/packages/integrations/image/src/get-image.ts +++ b/packages/integrations/image/src/get-image.ts @@ -105,29 +105,33 @@ export async function getImage( loader: ImageService, transform: GetImageTransform ): Promise { - (globalThis as any)['@astrojs/image'].loader = loader; + globalThis.astroImage.loader = loader; const resolved = await resolveTransform(transform); const attributes = await loader.getImageAttributes(resolved); - const isDev = (globalThis as any)['@astrojs/image'].command === 'dev'; + const isDev = globalThis.astroImage.command === 'dev'; const isLocalImage = !isRemoteImage(resolved.src); - const _loader = isDev && isLocalImage ? (globalThis as any)['@astrojs/image'].ssrLoader : loader; + const _loader = isDev && isLocalImage ? globalThis.astroImage.ssrLoader : loader; + + if (!_loader) { + throw new Error('@astrojs/image: loader not found!'); + } // For SSR services, build URLs for the injected route if (isSSRService(_loader)) { const { searchParams } = _loader.serializeTransform(resolved); // cache all images rendered to HTML - if (globalThis && (globalThis as any)['@astrojs/image'].addStaticImage) { - (globalThis as any)['@astrojs/image'].addStaticImage(resolved); + if (globalThis && globalThis.astroImage.addStaticImage) { + globalThis.astroImage.addStaticImage(resolved); } const src = - globalThis && (globalThis as any)['@astrojs/image'].filenameFormat - ? (globalThis as any)['@astrojs/image'].filenameFormat(resolved, searchParams) + globalThis && globalThis.astroImage.filenameFormat + ? globalThis.astroImage.filenameFormat(resolved, searchParams) : `${ROUTE_PATTERN}?${searchParams.toString()}`; return { diff --git a/packages/integrations/image/src/index.ts b/packages/integrations/image/src/index.ts index ee4147fc9120..067f987fb82f 100644 --- a/packages/integrations/image/src/index.ts +++ b/packages/integrations/image/src/index.ts @@ -17,7 +17,7 @@ export * from './get-image.js'; export * from './get-picture.js'; const createIntegration = (options: IntegrationOptions = {}): AstroIntegration => { - (globalThis as any)['@astrojs/image'] = { + globalThis.astroImage = { ssrLoader: sharp }; @@ -48,7 +48,7 @@ const createIntegration = (options: IntegrationOptions = {}): AstroIntegration = hooks: { 'astro:config:setup': ({ command, config, injectRoute, updateConfig }) => { _config = config; - (globalThis as any)['@astrojs/image'].command = command; + globalThis.astroImage.command = command; // Always treat `astro dev` as SSR mode, even without an adapter const mode = command === 'dev' || config.adapter ? 'ssr' : 'ssg'; @@ -57,12 +57,12 @@ const createIntegration = (options: IntegrationOptions = {}): AstroIntegration = // Used to cache all images rendered to HTML // Added to globalThis to share the same map in Node and Vite - (globalThis as any)['@astrojs/image'].addStaticImage = (transform: TransformOptions) => { + globalThis.astroImage.addStaticImage = (transform: TransformOptions) => { staticImages.set(propsToFilename(transform), transform); }; // TODO: Add support for custom, user-provided filename format functions - (globalThis as any)['@astrojs/image'].filenameFormat = ( + globalThis.astroImage.filenameFormat = ( transform: TransformOptions, searchParams: URLSearchParams ) => { @@ -89,7 +89,12 @@ const createIntegration = (options: IntegrationOptions = {}): AstroIntegration = }, 'astro:build:done': async ({ dir }) => { for await (const [filename, transform] of staticImages) { - const loader = (globalThis as any)['@astrojs/image'].loader; + const loader = globalThis.astroImage.loader; + + if (!loader || !('transform' in loader)) { + // this should never be hit, how was a staticImage added without an SSR service? + return; + } let inputBuffer: Buffer | undefined = undefined; let outputFile: string; diff --git a/packages/integrations/image/src/types.ts b/packages/integrations/image/src/types.ts index 96067e8285d5..eace5911973f 100644 --- a/packages/integrations/image/src/types.ts +++ b/packages/integrations/image/src/types.ts @@ -2,6 +2,18 @@ export type { Image, Picture } from '../components/index.js'; export * from './index.js'; +interface ImageIntegration { + loader?: ImageService; + ssrLoader?: SSRImageService; + command?: 'dev' | 'build'; + addStaticImage?: (transform: TransformOptions) => void; + filenameFormat?: (transform: TransformOptions, searchParams: URLSearchParams) => string; +} + +declare global { + var astroImage: ImageIntegration; +} + export type InputFormat = | 'heic' | 'heif' From 7c9ebfd5b6a9a5d17f87425d6afefb8683840e75 Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Mon, 18 Jul 2022 10:01:05 -0500 Subject: [PATCH 3/5] simplifying the globalThis type checking --- .../integrations/image/src/endpoints/dev.ts | 4 ---- packages/integrations/image/src/get-image.ts | 5 ++--- packages/integrations/image/src/index.ts | 21 ++++++++++++------- .../image/src/loaders/cloudinary.ts | 18 ++++++++++++++++ packages/integrations/image/src/types.ts | 8 +++---- 5 files changed, 37 insertions(+), 19 deletions(-) create mode 100644 packages/integrations/image/src/loaders/cloudinary.ts diff --git a/packages/integrations/image/src/endpoints/dev.ts b/packages/integrations/image/src/endpoints/dev.ts index cff8540b9ac4..67b37b177470 100644 --- a/packages/integrations/image/src/endpoints/dev.ts +++ b/packages/integrations/image/src/endpoints/dev.ts @@ -5,10 +5,6 @@ import { loadImage } from '../utils.js'; export const get: APIRoute = async ({ request }) => { const loader = globalThis.astroImage.ssrLoader; - if (!loader) { - throw new Error('@astrojs/image: loader not found!'); - } - try { const url = new URL(request.url); const transform = loader.parseTransform(url.searchParams); diff --git a/packages/integrations/image/src/get-image.ts b/packages/integrations/image/src/get-image.ts index 49c39a4deb45..83ac2e5ba06c 100644 --- a/packages/integrations/image/src/get-image.ts +++ b/packages/integrations/image/src/get-image.ts @@ -125,12 +125,11 @@ export async function getImage( const { searchParams } = _loader.serializeTransform(resolved); // cache all images rendered to HTML - if (globalThis && globalThis.astroImage.addStaticImage) { + if (globalThis?.astroImage) { globalThis.astroImage.addStaticImage(resolved); } - const src = - globalThis && globalThis.astroImage.filenameFormat + const src = globalThis?.astroImage ? globalThis.astroImage.filenameFormat(resolved, searchParams) : `${ROUTE_PATTERN}?${searchParams.toString()}`; diff --git a/packages/integrations/image/src/index.ts b/packages/integrations/image/src/index.ts index 067f987fb82f..da1ec1d4b8d4 100644 --- a/packages/integrations/image/src/index.ts +++ b/packages/integrations/image/src/index.ts @@ -17,10 +17,6 @@ export * from './get-image.js'; export * from './get-picture.js'; const createIntegration = (options: IntegrationOptions = {}): AstroIntegration => { - globalThis.astroImage = { - ssrLoader: sharp - }; - const resolvedOptions = { serviceEntryPoint: '@astrojs/image/sharp', ...options, @@ -48,7 +44,6 @@ const createIntegration = (options: IntegrationOptions = {}): AstroIntegration = hooks: { 'astro:config:setup': ({ command, config, injectRoute, updateConfig }) => { _config = config; - globalThis.astroImage.command = command; // Always treat `astro dev` as SSR mode, even without an adapter const mode = command === 'dev' || config.adapter ? 'ssr' : 'ssg'; @@ -57,15 +52,15 @@ const createIntegration = (options: IntegrationOptions = {}): AstroIntegration = // Used to cache all images rendered to HTML // Added to globalThis to share the same map in Node and Vite - globalThis.astroImage.addStaticImage = (transform: TransformOptions) => { + function addStaticImage(transform: TransformOptions) { staticImages.set(propsToFilename(transform), transform); }; // TODO: Add support for custom, user-provided filename format functions - globalThis.astroImage.filenameFormat = ( + function filenameFormat( transform: TransformOptions, searchParams: URLSearchParams - ) => { + ) { if (mode === 'ssg') { return isRemoteImage(transform.src) ? path.join(OUTPUT_DIR, path.basename(propsToFilename(transform))) @@ -79,6 +74,16 @@ const createIntegration = (options: IntegrationOptions = {}): AstroIntegration = } }; + // Initialize the integration's globalThis namespace + // This is needed to share scope between Node and Vite + globalThis.astroImage = { + loader: undefined, // initialized in first getImage() call + ssrLoader: sharp, + command, + addStaticImage, + filenameFormat, + } + if (mode === 'ssr') { injectRoute({ pattern: ROUTE_PATTERN, diff --git a/packages/integrations/image/src/loaders/cloudinary.ts b/packages/integrations/image/src/loaders/cloudinary.ts new file mode 100644 index 000000000000..0febb38241ed --- /dev/null +++ b/packages/integrations/image/src/loaders/cloudinary.ts @@ -0,0 +1,18 @@ +import type { HostedImageService, TransformOptions } from "../types"; + +export class Cloudinary implements HostedImageService { + async getImageAttributes(transform: TransformOptions): Promise { + const { width, height, src, format, quality, aspectRatio, ...rest } = transform; + + const url = `https://res.cloudinary.com/demo/image/upload/c_crop,g_face,h_400,w_400/r_max/c_scale,w_200/lady.jpg`; + + return { + ...rest, + src: url + }; + } +} + +const service = new Cloudinary(); + +export default service; diff --git a/packages/integrations/image/src/types.ts b/packages/integrations/image/src/types.ts index eace5911973f..e6c315c23376 100644 --- a/packages/integrations/image/src/types.ts +++ b/packages/integrations/image/src/types.ts @@ -4,10 +4,10 @@ export * from './index.js'; interface ImageIntegration { loader?: ImageService; - ssrLoader?: SSRImageService; - command?: 'dev' | 'build'; - addStaticImage?: (transform: TransformOptions) => void; - filenameFormat?: (transform: TransformOptions, searchParams: URLSearchParams) => string; + ssrLoader: SSRImageService; + command: 'dev' | 'build'; + addStaticImage: (transform: TransformOptions) => void; + filenameFormat: (transform: TransformOptions, searchParams: URLSearchParams) => string; } declare global { From e336fba51774ed7484e4f1487115faeb1d46b72e Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Mon, 18 Jul 2022 10:18:13 -0500 Subject: [PATCH 4/5] chore: adding changeset --- .changeset/purple-vans-bake.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/purple-vans-bake.md diff --git a/.changeset/purple-vans-bake.md b/.changeset/purple-vans-bake.md new file mode 100644 index 000000000000..3af99ba74feb --- /dev/null +++ b/.changeset/purple-vans-bake.md @@ -0,0 +1,5 @@ +--- +'@astrojs/image': patch +--- + +Improves the `astro dev` experience when using a third-party hosted image service From ef6bf698015d4f7294ca991879dfe13ecfce151d Mon Sep 17 00:00:00 2001 From: Tony Sullivan Date: Mon, 18 Jul 2022 10:29:48 -0500 Subject: [PATCH 5/5] removing temp hosted service used for testing --- packages/integrations/image/package.json | 1 - .../image/src/loaders/cloudinary.ts | 18 ------------------ 2 files changed, 19 deletions(-) delete mode 100644 packages/integrations/image/src/loaders/cloudinary.ts diff --git a/packages/integrations/image/package.json b/packages/integrations/image/package.json index 0fa2291937f1..a2b5486141ad 100644 --- a/packages/integrations/image/package.json +++ b/packages/integrations/image/package.json @@ -25,7 +25,6 @@ "import": "./dist/index.js" }, "./sharp": "./dist/loaders/sharp.js", - "./cloudinary": "./dist/loaders/cloudinary.js", "./endpoints/dev": "./dist/endpoints/dev.js", "./endpoints/prod": "./dist/endpoints/prod.js", "./components": "./components/index.js", diff --git a/packages/integrations/image/src/loaders/cloudinary.ts b/packages/integrations/image/src/loaders/cloudinary.ts deleted file mode 100644 index 0febb38241ed..000000000000 --- a/packages/integrations/image/src/loaders/cloudinary.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { HostedImageService, TransformOptions } from "../types"; - -export class Cloudinary implements HostedImageService { - async getImageAttributes(transform: TransformOptions): Promise { - const { width, height, src, format, quality, aspectRatio, ...rest } = transform; - - const url = `https://res.cloudinary.com/demo/image/upload/c_crop,g_face,h_400,w_400/r_max/c_scale,w_200/lady.jpg`; - - return { - ...rest, - src: url - }; - } -} - -const service = new Cloudinary(); - -export default service;