2022-08-30 21:12:45 +00:00
|
|
|
import type { AstroConfig } from 'astro';
|
|
|
|
import MagicString from 'magic-string';
|
2023-02-15 08:21:41 +00:00
|
|
|
import mime from 'mime';
|
2022-08-30 21:09:44 +00:00
|
|
|
import fs from 'node:fs/promises';
|
2023-01-30 20:31:49 +00:00
|
|
|
import { basename, extname } from 'node:path';
|
2022-08-30 21:09:44 +00:00
|
|
|
import { Readable } from 'node:stream';
|
2023-01-30 20:31:49 +00:00
|
|
|
import { pathToFileURL } from 'node:url';
|
2022-07-01 15:47:48 +00:00
|
|
|
import type { Plugin, ResolvedConfig } from 'vite';
|
2022-07-27 15:39:05 +00:00
|
|
|
import type { IntegrationOptions } from './index.js';
|
|
|
|
import type { InputFormat } from './loaders/index.js';
|
2022-07-22 23:04:01 +00:00
|
|
|
import { metadata } from './utils/metadata.js';
|
2022-07-01 15:47:48 +00:00
|
|
|
|
2022-07-27 15:39:05 +00:00
|
|
|
export interface ImageMetadata {
|
|
|
|
src: string;
|
|
|
|
width: number;
|
|
|
|
height: number;
|
|
|
|
format: InputFormat;
|
|
|
|
}
|
|
|
|
|
2022-07-01 15:47:48 +00:00
|
|
|
export function createPlugin(config: AstroConfig, options: Required<IntegrationOptions>): Plugin {
|
2022-07-01 17:43:26 +00:00
|
|
|
const filter = (id: string) =>
|
2023-02-15 08:21:41 +00:00
|
|
|
/^(?!\/_image?).*.(heic|heif|avif|jpeg|jpg|png|tiff|webp|gif|svg)$/.test(id);
|
2022-07-01 15:47:48 +00:00
|
|
|
|
|
|
|
const virtualModuleId = 'virtual:image-loader';
|
|
|
|
|
|
|
|
let resolvedConfig: ResolvedConfig;
|
|
|
|
|
|
|
|
return {
|
|
|
|
name: '@astrojs/image',
|
|
|
|
enforce: 'pre',
|
2022-07-07 21:06:44 +00:00
|
|
|
configResolved(viteConfig) {
|
|
|
|
resolvedConfig = viteConfig;
|
2022-07-01 15:47:48 +00:00
|
|
|
},
|
|
|
|
async resolveId(id) {
|
|
|
|
// The virtual model redirects imports to the ImageService being used
|
|
|
|
// This ensures the module is available in `astro dev` and is included
|
|
|
|
// in the SSR server bundle.
|
|
|
|
if (id === virtualModuleId) {
|
2022-08-30 21:09:44 +00:00
|
|
|
return await this.resolve(options.serviceEntryPoint);
|
2022-07-01 15:47:48 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
async load(id) {
|
|
|
|
// only claim image ESM imports
|
2022-07-01 17:43:26 +00:00
|
|
|
if (!filter(id)) {
|
|
|
|
return null;
|
|
|
|
}
|
2022-07-01 15:47:48 +00:00
|
|
|
|
2022-08-30 21:09:44 +00:00
|
|
|
const url = pathToFileURL(id);
|
|
|
|
|
|
|
|
const meta = await metadata(url);
|
|
|
|
|
|
|
|
if (!meta) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.meta.watchMode) {
|
2022-09-01 20:31:53 +00:00
|
|
|
const pathname = decodeURI(url.pathname);
|
|
|
|
const filename = basename(pathname, extname(pathname) + `.${meta.format}`);
|
2022-07-01 17:43:26 +00:00
|
|
|
|
2022-08-30 21:09:44 +00:00
|
|
|
const handle = this.emitFile({
|
|
|
|
name: filename,
|
|
|
|
source: await fs.readFile(url),
|
|
|
|
type: 'asset',
|
|
|
|
});
|
2022-07-01 15:47:48 +00:00
|
|
|
|
2022-08-30 21:09:44 +00:00
|
|
|
meta.src = `__ASTRO_IMAGE_ASSET__${handle}__`;
|
|
|
|
} else {
|
2023-01-30 20:29:41 +00:00
|
|
|
meta.src = '/@astroimage' + url.pathname;
|
2022-08-30 21:09:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return `export default ${JSON.stringify(meta)}`;
|
2022-07-22 23:04:01 +00:00
|
|
|
},
|
2022-08-30 21:09:44 +00:00
|
|
|
configureServer(server) {
|
|
|
|
server.middlewares.use(async (req, res, next) => {
|
|
|
|
if (req.url?.startsWith('/@astroimage/')) {
|
2023-01-30 20:29:41 +00:00
|
|
|
// Reconstructing URL to get rid of query parameters in path
|
|
|
|
const url = new URL(req.url.slice('/@astroimage'.length), 'file:');
|
2022-08-30 21:09:44 +00:00
|
|
|
|
|
|
|
const file = await fs.readFile(url);
|
|
|
|
|
|
|
|
const meta = await metadata(url);
|
|
|
|
|
|
|
|
if (!meta) {
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2022-09-22 19:50:47 +00:00
|
|
|
const transform = await globalThis.astroImage.defaultLoader.parseTransform(
|
|
|
|
url.searchParams
|
|
|
|
);
|
2022-08-30 21:09:44 +00:00
|
|
|
|
2022-09-30 17:27:56 +00:00
|
|
|
// if no transforms were added, the original file will be returned as-is
|
|
|
|
let data = file;
|
|
|
|
let format = meta.format;
|
|
|
|
|
|
|
|
if (transform) {
|
|
|
|
const result = await globalThis.astroImage.defaultLoader.transform(file, transform);
|
|
|
|
data = result.data;
|
|
|
|
format = result.format;
|
2022-08-30 21:09:44 +00:00
|
|
|
}
|
|
|
|
|
2023-02-15 08:21:41 +00:00
|
|
|
res.setHeader('Content-Type', mime.getType(format) || '');
|
2022-08-30 21:09:44 +00:00
|
|
|
res.setHeader('Cache-Control', 'max-age=360000');
|
|
|
|
|
2022-09-30 17:27:56 +00:00
|
|
|
const stream = Readable.from(data);
|
2022-08-30 21:09:44 +00:00
|
|
|
return stream.pipe(res);
|
|
|
|
}
|
|
|
|
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
async renderChunk(code) {
|
|
|
|
const assetUrlRE = /__ASTRO_IMAGE_ASSET__([a-z\d]{8})__(?:_(.*?)__)?/g;
|
|
|
|
|
|
|
|
let match;
|
|
|
|
let s;
|
|
|
|
while ((match = assetUrlRE.exec(code))) {
|
|
|
|
s = s || (s = new MagicString(code));
|
|
|
|
const [full, hash, postfix = ''] = match;
|
|
|
|
|
|
|
|
const file = this.getFileName(hash);
|
|
|
|
const outputFilepath = resolvedConfig.base + file + postfix;
|
|
|
|
|
|
|
|
s.overwrite(match.index, match.index + full.length, outputFilepath);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (s) {
|
|
|
|
return {
|
|
|
|
code: s.toString(),
|
|
|
|
map: resolvedConfig.build.sourcemap ? s.generateMap({ hires: true }) : null,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
2022-08-30 21:12:45 +00:00
|
|
|
},
|
2022-07-01 15:47:48 +00:00
|
|
|
};
|
|
|
|
}
|