diff --git a/data/paths.json b/data/paths.json index b616ed3..c9683f5 100644 --- a/data/paths.json +++ b/data/paths.json @@ -6,5 +6,6 @@ "/_vercel/image": "vercel", "/is/image": "scene7", "/_ipx/": "ipx", + "/_image": "astro", "/.netlify/images": "netlify" } diff --git a/src/parse.ts b/src/parse.ts index 3e1a9e3..d2e24c9 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -18,6 +18,7 @@ import { parse as imageengine } from "./transformers/imageengine.ts"; import { parse as contentstack } from "./transformers/contentstack.ts"; import { parse as cloudflareImages } from "./transformers/cloudflare_images.ts"; import { parse as ipx } from "./transformers/ipx.ts"; +import { parse as astro } from "./transformers/astro.ts"; import { parse as netlify } from "./transformers/netlify.ts"; import { parse as imagekit } from "./transformers/imagekit.ts"; import { ImageCdn, ParsedUrl, SupportedImageCdn, UrlParser } from "./types.ts"; @@ -42,6 +43,7 @@ export const parsers = { contentstack, "cloudflare_images": cloudflareImages, ipx, + astro, netlify, imagekit, }; diff --git a/src/transform.ts b/src/transform.ts index 09d8dd9..5957bd8 100644 --- a/src/transform.ts +++ b/src/transform.ts @@ -18,6 +18,7 @@ import { transform as imageengine } from "./transformers/imageengine.ts"; import { transform as contentstack } from "./transformers/contentstack.ts"; import { transform as cloudflareImages } from "./transformers/cloudflare_images.ts"; import { transform as ipx } from "./transformers/ipx.ts"; +import { transform as astro } from "./transformers/astro.ts"; import { transform as netlify } from "./transformers/netlify.ts"; import { transform as imagekit } from "./transformers/imagekit.ts"; import { ImageCdn, UrlTransformer } from "./types.ts"; @@ -43,6 +44,7 @@ export const getTransformer = (cdn: ImageCdn) => ({ contentstack, "cloudflare_images": cloudflareImages, ipx, + astro, netlify, imagekit, }[cdn]); diff --git a/src/transformers/astro.test.ts b/src/transformers/astro.test.ts new file mode 100644 index 0000000..f05dfbc --- /dev/null +++ b/src/transformers/astro.test.ts @@ -0,0 +1,87 @@ +import { assertEquals } from "https://deno.land/std@0.172.0/testing/asserts.ts"; + +import { ParsedUrl } from "../types.ts"; +import { AstroParams, parse, transform } from "./astro.ts"; + +const img = + "https://images.ctfassets.net/aaaa/xxxx/yyyy/how-to-wow-a-customer.jpg"; + +Deno.test("astro parser", () => { + const parsed = parse(img); + const expected: ParsedUrl = { + base: "/_image?href=https%3A%2F%2Fimages.ctfassets.net%2Faaaa%2Fxxxx%2Fyyyy%2Fhow-to-wow-a-customer.jpg", + params: { + "href": "https%3A%2F%2Fimages.ctfassets.net%2Faaaa%2Fxxxx%2Fyyyy%2Fhow-to-wow-a-customer.jpg" + }, + cdn: "astro" + } + assertEquals(JSON.stringify(parsed), JSON.stringify(expected)); +}); + +Deno.test("astro parser endpoint", () => { + const parsed = parse("/_image?href=https%3A%2F%2Fimages.ctfassets.net%2Faaaa%2Fxxxx%2Fyyyy%2Fhow-to-wow-a-customer.jpg"); + const expected: ParsedUrl = { + base: "/_image?href=https%3A%2F%2Fimages.ctfassets.net%2Faaaa%2Fxxxx%2Fyyyy%2Fhow-to-wow-a-customer.jpg", + params: { + "href": "https%3A%2F%2Fimages.ctfassets.net%2Faaaa%2Fxxxx%2Fyyyy%2Fhow-to-wow-a-customer.jpg" + }, + cdn: "astro" + } + assertEquals(JSON.stringify(parsed), JSON.stringify(expected)); +}); + + +Deno.test("astro", async (t) => { + await t.step("should format a URL", () => { + const result = transform({ + url: img, + width: 200, + height: 100, + }); + assertEquals( + result?.toString(), + "/_image?href=https%3A%2F%2Fimages.ctfassets.net%2Faaaa%2Fxxxx%2Fyyyy%2Fhow-to-wow-a-customer.jpg&w=200&h=100", + ); + }); + await t.step("should not set height if not provided", () => { + const result = transform({ url: img, width: 200 }); + assertEquals( + result?.toString(), + "/_image?href=https%3A%2F%2Fimages.ctfassets.net%2Faaaa%2Fxxxx%2Fyyyy%2Fhow-to-wow-a-customer.jpg&w=200", + ); + }); + await t.step("should delete height if not set", () => { + const url = new URL(img); + url.searchParams.set("h", "100"); + const result = transform({ url, width: 200 }); + assertEquals( + result?.toString(), + "/_image?href=https%3A%2F%2Fimages.ctfassets.net%2Faaaa%2Fxxxx%2Fyyyy%2Fhow-to-wow-a-customer.jpg&w=200", + ); + }); + + await t.step("should round non-integer params", () => { + const result = transform({ + url: img, + width: 200.6, + height: 100.2, + }); + assertEquals( + result?.toString(), + "/_image?href=https%3A%2F%2Fimages.ctfassets.net%2Faaaa%2Fxxxx%2Fyyyy%2Fhow-to-wow-a-customer.jpg&w=201&h=100", + ); + }); + + await t.step("should transform a local image with a relative base", () => { + const result = transform({ + url: "/static/moose.png", + width: 100, + height: 200, + format: "webp", + }); + assertEquals( + result?.toString(), + "/_image?href=%2Fstatic%2Fmoose.png&w=100&h=200&f=webp", + ); + }); +}); diff --git a/src/transformers/astro.ts b/src/transformers/astro.ts new file mode 100644 index 0000000..93287dd --- /dev/null +++ b/src/transformers/astro.ts @@ -0,0 +1,71 @@ +import { UrlParser, UrlTransformer, ShouldDelegateUrl } from "../types.ts"; +import { + getNumericParam, + setParamIfDefined, + toUrl, + toCanonicalUrlString, +} from "../utils.ts"; +import { getImageCdnForUrlByDomain } from "../detect.ts"; + +export interface AstroParams { + href: string; + quality?: string | number; +} + +export const delegateUrl: ShouldDelegateUrl = (url) => { + const parsedUrl = toUrl(url); + const searchParamHref = parsedUrl.searchParams.get("href") + const decodedHref = typeof searchParamHref === "string" + ? decodeURIComponent(searchParamHref) + : new URL(parsedUrl.pathname, parsedUrl.origin).toString() + const source = toCanonicalUrlString(toUrl(decodedHref)); + + if (!source || !source.startsWith("http")) { + return false; + } + const cdn = getImageCdnForUrlByDomain(source); + if (!cdn) { + return false; + } + return { + cdn, + url: source, + }; +}; + +export const parse: UrlParser = (url) => { + const parsedUrl = toUrl(url); + const searchParamHref = parsedUrl.searchParams.get("href") + const decodedHref = typeof searchParamHref === "string" + ? decodeURIComponent(searchParamHref) + : new URL(parsedUrl.pathname, parsedUrl.origin).toString() + const encodedHref = encodeURIComponent(toCanonicalUrlString(toUrl(decodedHref))); + const width = getNumericParam(parsedUrl, "w") || undefined; + const height = getNumericParam(parsedUrl, "h") || undefined; + const format = parsedUrl.searchParams.get("f") || undefined; + const quality = parsedUrl.searchParams.get("q") || undefined; + + return { + width, + height, + format, + base: `/_image?href=${encodedHref}`, + params: { quality, href: encodedHref }, + cdn: "astro", + }; +}; + +export const transform: UrlTransformer = ( + { url: originalUrl, width, height, format, }, +) => { + const parsedUrl = toUrl(originalUrl); + const href = toCanonicalUrlString(new URL(parsedUrl.pathname, parsedUrl.origin)) + const url = {searchParams: new URLSearchParams()} as URL + + setParamIfDefined(url, "href", href, true, true); + setParamIfDefined(url, "w", width, true, true); + setParamIfDefined(url, "h", height, true, true); + setParamIfDefined(url, "f", format); + + return `/_image?${url.searchParams.toString()}`; +}; diff --git a/src/types.ts b/src/types.ts index aa1f37a..5429162 100644 --- a/src/types.ts +++ b/src/types.ts @@ -95,6 +95,7 @@ export type ImageCdn = | "contentstack" | "cloudflare_images" | "ipx" + | "astro" | "netlify" | "imagekit";