2022-08-30 21:09:44 +00:00
|
|
|
import { basename, extname, join } from 'node:path';
|
|
|
|
import fs from 'node:fs/promises';
|
|
|
|
import path from 'node:path';
|
|
|
|
import { Readable } from 'node:stream';
|
|
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
2022-07-01 20:06:01 +00:00
|
|
|
import type { AstroConfig } from 'astro';
|
2022-08-30 21:09:44 +00:00
|
|
|
import MagicString from 'magic-string';
|
2022-07-01 15:47:48 +00:00
|
|
|
import type { PluginContext } from 'rollup';
|
2022-07-01 20:06:01 +00:00
|
|
|
import slash from 'slash';
|
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-08-30 21:09:44 +00:00
|
|
|
import sharp from './loaders/sharp.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) =>
|
|
|
|
/^(?!\/_image?).*.(heic|heif|avif|jpeg|jpg|png|tiff|webp|gif)$/.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) {
|
|
|
|
const filename = basename(url.pathname, extname(url.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 {
|
|
|
|
const relId = path.relative(fileURLToPath(config.srcDir), id);
|
2022-07-01 15:47:48 +00:00
|
|
|
|
2022-08-30 21:09:44 +00:00
|
|
|
meta.src = join('/@astroimage', relId);
|
|
|
|
|
|
|
|
// Windows compat
|
|
|
|
meta.src = slash(meta.src);
|
|
|
|
}
|
|
|
|
|
|
|
|
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/')) {
|
|
|
|
const [, id] = req.url.split('/@astroimage/');
|
|
|
|
|
|
|
|
const url = new URL(id, config.srcDir);
|
|
|
|
const file = await fs.readFile(url);
|
|
|
|
|
|
|
|
const meta = await metadata(url);
|
|
|
|
|
|
|
|
if (!meta) {
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
const transform = await sharp.parseTransform(url.searchParams);
|
|
|
|
|
|
|
|
if (!transform) {
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = await sharp.transform(file, transform);
|
|
|
|
|
|
|
|
res.setHeader('Content-Type', `image/${result.format}`);
|
|
|
|
res.setHeader('Cache-Control', 'max-age=360000');
|
|
|
|
|
|
|
|
const stream = Readable.from(result.data);
|
|
|
|
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-07-01 15:47:48 +00:00
|
|
|
};
|
|
|
|
}
|