@nuxt/image
Version:
Nuxt Image Module
331 lines (324 loc) • 10.4 kB
JavaScript
import process$1 from 'node:process';
import { parseURL, withLeadingSlash } from 'ufo';
import { createResolver, useNitro, useNuxt, resolvePath, defineNuxtModule, addImports, addComponent, addTemplate, addPlugin } from '@nuxt/kit';
import { resolve, relative, normalize } from 'pathe';
import { defu } from 'defu';
import { hash } from 'ohash';
import { genSafeVariableName } from 'knitwork';
import { provider } from 'std-env';
import { existsSync } from 'node:fs';
const ipxSetup = (setupOptions) => (providerOptions, moduleOptions) => {
const resolver = createResolver(import.meta.url);
const nitro = useNitro();
const nuxt = useNuxt();
const ipxBaseURL = providerOptions.options?.baseURL || "/_ipx";
const hasUserProvidedIPX = nuxt.options.serverHandlers.find((handler) => handler.route?.startsWith(ipxBaseURL)) || nuxt.options.devServerHandlers.find((handler) => handler.route?.startsWith(ipxBaseURL));
if (hasUserProvidedIPX) {
return;
}
const publicDirs = nuxt.options._layers.map((layer) => {
const isRootLayer = layer.config.rootDir === nuxt.options.rootDir;
const layerOptions = isRootLayer ? nuxt.options : layer.config;
const path = isRootLayer ? moduleOptions.dir : layerOptions.dir?.public || "public";
return resolve(layerOptions.srcDir, path);
}).filter((dir) => existsSync(dir));
const relativeDir = relative(nitro.options.output.serverDir, nitro.options.output.publicDir);
const ipxOptions = {
...providerOptions.options,
baseURL: ipxBaseURL,
alias: {
...moduleOptions.alias,
...providerOptions.options?.alias
},
fs: providerOptions.options?.fs !== false && {
dir: nuxt.options.dev ? publicDirs : relativeDir,
...providerOptions.options?.fs
},
http: providerOptions.options?.http !== false && {
domains: moduleOptions.domains,
...providerOptions.options?.http
}
};
nitro.options._config.runtimeConfig = nitro.options._config.runtimeConfig || {};
nitro.options.runtimeConfig.ipx = ipxOptions;
const ipxHandler = {
route: `${ipxBaseURL}/**`,
middleware: false,
handler: resolver.resolve("./runtime/ipx")
};
if (!setupOptions?.isStatic) {
nitro.options.handlers.push(ipxHandler);
}
if (!nitro.options.dev) {
nitro.options._config.runtimeConfig.ipx = defu({ fs: { dir: publicDirs } }, ipxOptions);
nitro.options._config.handlers.push(ipxHandler);
}
};
const BuiltInProviders = [
"aliyun",
"awsAmplify",
"bunny",
"caisy",
"cloudflare",
"cloudimage",
"cloudinary",
"contentful",
"directus",
"edgio",
"fastly",
"filerobot",
"glide",
"gumlet",
"hygraph",
"imageengine",
"imagekit",
"imgix",
"ipx",
"ipxStatic",
"layer0",
"netlify",
"netlifyLargeMedia",
"netlifyImageCdn",
"prepr",
"none",
"prismic",
"sanity",
"storyblok",
"strapi",
"strapi5",
"twicpics",
"unsplash",
"uploadcare",
"vercel",
"wagtail",
"weserv",
"sirv"
];
const providerSetup = {
// IPX
ipx: ipxSetup(),
ipxStatic: ipxSetup({ isStatic: true }),
// https://vercel.com/docs/build-output-api/v3/configuration#images
vercel(_providerOptions, moduleOptions, nuxt) {
nuxt.options.nitro = defu(nuxt.options.nitro, {
vercel: {
config: {
images: {
domains: moduleOptions.domains,
minimumCacheTTL: 60 * 5,
sizes: Array.from(new Set(Object.values(moduleOptions.screens || {}))),
formats: ["image/webp", "image/avif"]
}
}
}
});
},
awsAmplify(_providerOptions, moduleOptions, nuxt) {
nuxt.options.nitro = defu(nuxt.options.nitro, {
awsAmplify: {
imageOptimization: {
path: "/_amplify/image",
cacheControl: "public, max-age=300, immutable"
},
imageSettings: {
sizes: Array.from(new Set(Object.values(moduleOptions.screens || {}))),
formats: ["image/jpeg", "image/png", "image/webp", "image/avif"],
minimumCacheTTL: 60 * 5,
domains: moduleOptions.domains,
remotePatterns: [],
// Provided by domains
dangerouslyAllowSVG: false
// TODO
}
}
});
},
// https://docs.netlify.com/image-cdn/create-integration/
netlify(_providerOptions, moduleOptions, nuxt) {
if (moduleOptions.domains?.length > 0) {
nuxt.options.nitro = defu(nuxt.options.nitro, {
netlify: {
images: {
remote_images: moduleOptions.domains.map((domain) => `https?:\\/\\/${domain.replaceAll(".", "\\.")}\\/.*`)
}
}
});
}
}
};
async function resolveProviders(nuxt, options) {
const providers = [];
for (const key in options) {
if (BuiltInProviders.includes(key)) {
providers.push(await resolveProvider(nuxt, key, { provider: key, options: options[key] }));
}
}
for (const key in options.providers) {
providers.push(await resolveProvider(nuxt, key, options.providers[key]));
}
return providers;
}
async function resolveProvider(_nuxt, key, input) {
if (typeof input === "string") {
input = { name: input };
}
if (!input.name) {
input.name = key;
}
if (!input.provider) {
input.provider = input.name;
}
if (input.provider in normalizableProviders) {
input.provider = normalizableProviders[input.provider]();
}
const resolver = createResolver(import.meta.url);
input.provider = BuiltInProviders.includes(input.provider) ? await resolver.resolve("./runtime/providers/" + input.provider) : await resolvePath(input.provider);
const setup = input.setup || providerSetup[input.name];
return {
...input,
setup,
runtime: normalize(input.provider),
importName: `${key}Runtime$${genSafeVariableName(hash(input.provider))}`,
runtimeOptions: input.options
};
}
const autodetectableProviders = {
vercel: "vercel",
aws_amplify: "awsAmplify",
netlify: "netlify"
};
const normalizableProviders = {
netlify: () => {
return process.env.NETLIFY_LFS_ORIGIN_URL ? "netlifyLargeMedia" : "netlifyImageCdn";
}
};
function detectProvider(userInput = "") {
if (process.env.NUXT_IMAGE_PROVIDER) {
return process.env.NUXT_IMAGE_PROVIDER;
}
if (userInput && userInput !== "auto") {
return userInput;
}
if (provider in autodetectableProviders) {
return autodetectableProviders[provider];
}
}
const module = defineNuxtModule({
defaults: (nuxt) => ({
inject: false,
provider: "auto",
dir: nuxt.options.dir.public,
presets: {},
domains: [],
sharp: {},
format: ["webp"],
// https://tailwindcss.com/docs/breakpoints
screens: {
"xs": 320,
"sm": 640,
"md": 768,
"lg": 1024,
"xl": 1280,
"xxl": 1536,
"2xl": 1536
},
providers: {},
alias: {},
densities: [1, 2]
}),
meta: {
name: "@nuxt/image",
configKey: "image",
compatibility: {
nuxt: ">=3.1.0"
}
},
async setup(options, nuxt) {
const resolver = createResolver(import.meta.url);
options.dir = resolve(nuxt.options.srcDir, options.dir);
const domainsFromENV = process$1.env.NUXT_IMAGE_DOMAINS?.replace(/\s/g, "").split(",") || [];
options.domains = [.../* @__PURE__ */ new Set([...options.domains, ...domainsFromENV])].map((d) => d && parseURL(d.startsWith("http") ? d : "http://" + d).host).filter(Boolean);
options.alias = Object.fromEntries(Object.entries(options.alias).map((e) => [withLeadingSlash(e[0]), e[1]]));
options.provider = detectProvider(options.provider);
if (options.provider) {
options[options.provider] = options[options.provider] || {};
}
options.densities = options.densities || [];
const imageOptions = pick(options, [
"screens",
"presets",
"provider",
"domains",
"alias",
"densities",
"format",
"quality"
]);
const providers = await resolveProviders(nuxt, options);
for (const p of providers) {
if (typeof p.setup === "function" && p.name !== "ipx" && p.name !== "ipxStatic") {
await p.setup(p, options, nuxt);
}
}
const runtimeDir = resolver.resolve("./runtime");
nuxt.options.alias["#image"] = runtimeDir;
nuxt.options.build.transpile.push(runtimeDir);
addImports({
name: "useImage",
from: resolver.resolve("runtime/composables")
});
addComponent({
name: "NuxtImg",
filePath: resolver.resolve("./runtime/components/NuxtImg.vue")
});
addComponent({
name: "NuxtPicture",
filePath: resolver.resolve("./runtime/components/NuxtPicture.vue")
});
addTemplate({
filename: "image-options.mjs",
getContents() {
return `
${providers.map((p) => `import * as ${p.importName} from '${p.runtime}'`).join("\n")}
export const imageOptions = ${JSON.stringify(imageOptions, null, 2)}
imageOptions.providers = {
${providers.map((p) => ` ['${p.name}']: { provider: ${p.importName}, defaults: ${JSON.stringify(p.runtimeOptions)} }`).join(",\n")}
}
`;
}
});
nuxt.hook("nitro:init", async (nitro) => {
if (!options.provider || options.provider === "ipx" || options.provider === "ipxStatic" || options.ipx) {
const resolvedProvider = nitro.options.static || options.provider === "ipxStatic" ? "ipxStatic" : nitro.options.node ? "ipx" : "none";
if (!options.provider || options.provider === "ipx" || options.provider === "ipxStatic") {
imageOptions.provider = options.provider = resolvedProvider;
}
if (resolvedProvider === "ipxStatic") {
options.ipxStatic ||= options.ipx || {};
} else {
options[resolvedProvider] = options[resolvedProvider] || {};
}
const p = await resolveProvider(nuxt, resolvedProvider, {
options: options[resolvedProvider]
});
if (!providers.some((p2) => p2.name === resolvedProvider)) {
providers.push(p);
}
if (typeof p.setup === "function") {
await p.setup(p, options, nuxt);
}
}
});
if (options.inject) {
addPlugin({ src: resolver.resolve("./runtime/plugin") });
}
}
});
function pick(obj, keys) {
const newobj = {};
for (const key of keys) {
newobj[key] = obj[key];
}
return newobj;
}
export { module as default };