-
-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Change to directive based workflow
BREAKING CHANGE: This completely reworks the plugin interface. Documentation is coming soon
- Loading branch information
1 parent
9764a33
commit 0f1b5b1
Showing
13 changed files
with
237 additions
and
218 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Directive } from "./index" | ||
import pm from 'picomatch' | ||
|
||
export const fitDirective: Directive = { | ||
name: 'fit', | ||
test(key: string, value: string) { | ||
if (key === 'fit') return true | ||
|
||
return pm.isMatch(key, ['cover', 'contain', 'fill', 'inside', 'outside']) && value === '' | ||
}, | ||
transform(key: string, value: string) { | ||
return { fit: !!value ? value : key } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { Directive } from "./index" | ||
import pm from 'picomatch' | ||
|
||
|
||
export const formatDirective: Directive = { | ||
name: 'format', | ||
test(key: string, value: string) { | ||
if (key === 'format') return true | ||
|
||
return pm.isMatch(key, ['jpeg', 'webp', 'avif', 'png', 'gif', 'tiff', 'heif']) && value === '' | ||
}, | ||
transform(key: string, value: string) { | ||
return { format: !!value ? value : key } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Directive } from "./index" | ||
|
||
export const heightDirective: Directive = { | ||
name: 'height', | ||
test(key: string, value: string) { | ||
return key == 'height' && !isNaN(parseInt(value)) | ||
}, | ||
transform(key: string, value: string) { | ||
return { height: parseInt(value) } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { sizeDirective } from './size' | ||
import { fitDirective } from './fit' | ||
import { widthDirective } from './width' | ||
import { heightDirective } from './height' | ||
import { formatDirective } from './format' | ||
import { positionDirective } from './position' | ||
import { kernelDirective } from './kernel' | ||
import { srcsetDirective } from './srcset' | ||
|
||
export interface Directive { | ||
name: string, | ||
test: (key: string, value: string) => boolean | ||
transform?: (key: string, value: string) => Record<string, any> | ||
} | ||
|
||
export function directives(): Directive[] { | ||
return [ | ||
sizeDirective, | ||
widthDirective, | ||
heightDirective, | ||
formatDirective, | ||
positionDirective, | ||
fitDirective, | ||
kernelDirective, | ||
srcsetDirective | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { Directive } from "./index" | ||
import pm from 'picomatch' | ||
|
||
export const kernelDirective: Directive = { | ||
name: 'kernel', | ||
test(key: string, value: string) { | ||
return key === 'kernel' && pm.isMatch(value, ['nearest', 'cubic', 'mitchell', 'lanczos2', 'lanczos3']) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { Directive } from "./index" | ||
import pm from 'picomatch' | ||
|
||
export const positionDirective: Directive = { | ||
name: 'position', | ||
test(key: string, value: string) { | ||
if (key === 'position') return true | ||
|
||
return pm.isMatch(key, ['top', 'right top', 'right', 'right bottom', 'bottom', 'left bottom', 'left', 'left top', 'north', 'northeast', 'east', 'southeast', 'south', 'southwest', 'west', 'northwest', 'center', 'centre', 'entropy', 'attention']) && value === '' | ||
}, | ||
transform(key: string, value: string) { | ||
return { position: !!value ? value : key } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { Directive } from "./index" | ||
import { heightDirective } from "./height" | ||
import { widthDirective } from "./width" | ||
import pm from 'picomatch' | ||
|
||
export const sizeDirective: Directive = { | ||
name: 'size', | ||
test(key: string, value: string) { | ||
return key === 'size' && pm.isMatch(value, ['+([0-9])x+([0-9])']) | ||
}, | ||
transform(_: string, value: string) { | ||
if (~value.indexOf('x')) { | ||
const [width, height] = value.split('x') | ||
return { | ||
...widthDirective.transform('width', width), | ||
...heightDirective.transform('height', height) | ||
} | ||
} else { | ||
return widthDirective.transform('width', value) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Directive } from "." | ||
|
||
export const srcsetDirective: Directive = { | ||
name: 'size', | ||
test(key: string, value: string) { | ||
return key === 'size' && value === '' | ||
}, | ||
transform() { | ||
return { srcset: true } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Directive } from "./index" | ||
|
||
export const widthDirective: Directive = { | ||
name: 'width', | ||
test(key: string, value: string) { | ||
return key === 'width' && !isNaN(parseInt(value)) | ||
}, | ||
transform(_: string, value: string) { | ||
return { width: parseInt(value) } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,135 +1,94 @@ | ||
import { createFilter, dataToEsm } from 'rollup-pluginutils' | ||
import MagicString from 'magic-string' | ||
import pm from "picomatch" | ||
import sharp from 'sharp' | ||
import { Options, Target } from './types' | ||
import { cleanUrl, assetUrlQuotedRE, transformId } from './util' | ||
import cacache from 'cacache' | ||
import { buildOptions, has, Options } from "./options" | ||
import { directives } from './directives' | ||
import { createHash } from 'crypto' | ||
import path from 'path' | ||
import { PluginContext } from 'rollup' | ||
|
||
const defaultOptions: Options = { | ||
include: ['**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.gif', '**/*.webp'], | ||
exclude: '', | ||
targets: [{ format: 'jpeg' }, { format: 'webp' }, { format: 'avif' }] | ||
} | ||
|
||
export default function imagesetPlugin(opts?: Options) { | ||
const pluginOptions = Object.assign({}, defaultOptions, opts) | ||
const filter = createFilter(pluginOptions.include, pluginOptions.exclude) | ||
|
||
let config: Record<string, any> | ||
|
||
return { | ||
name: 'imageset', | ||
enforce: 'pre', | ||
|
||
configResolved(resolvedConfig) { | ||
config = resolvedConfig | ||
pluginOptions.cachePath ??= path.join(config.optimizeCacheDir, 'imageset_cache') | ||
}, | ||
|
||
async load(id: string) { | ||
if (!filter(id)) { | ||
return null | ||
} else { | ||
const paths = await Promise.all( | ||
pluginOptions.targets.map(target => { | ||
if (config.command === 'serve') { | ||
return transformDev(id, target) | ||
import { promises as fs } from 'fs' | ||
import { dataToEsm } from "rollup-pluginutils" | ||
|
||
export default function () { | ||
const filter = pm(['**/*.jpg', '**/*.jpg', '**/*.png', '**/*.webp', '**/*.webp', '**/*.avif', '**/*.gif', '**/*.heif']) | ||
|
||
const CACHE_DIR = './node_modules/.cache/vite-plugin-imageset' | ||
|
||
let globalConfig | ||
|
||
return { | ||
name: 'imageset', | ||
enforce: 'pre' as 'pre', | ||
async buildStart() { | ||
await fs.mkdir(CACHE_DIR, { recursive: true }) | ||
}, | ||
configResolved(config) { | ||
globalConfig = config | ||
}, | ||
async load(assetPath: string) { | ||
const url = new URL(assetPath, 'file://') | ||
|
||
if (!filter(url.pathname)) { | ||
return null | ||
} else { | ||
return transformBuild(id, target, this) | ||
const options = buildOptions(url, directives()) | ||
const id = generateId(assetPath) | ||
const ext = options.format ? `.${options.format}` : path.extname(url.pathname) | ||
const cachePath = path.join(CACHE_DIR, id + ext) | ||
|
||
if (globalConfig.command === 'serve') { | ||
if (!(await isCached(cachePath))) { | ||
const content = await transformImage(url, options) | ||
await fs.writeFile(cachePath, content) | ||
} | ||
|
||
return dataToEsm('/' + path.relative(globalConfig.root, cachePath), globalConfig.build.rollupOptions) | ||
} else { | ||
let content: Uint8Array | ||
if (await isCached(cachePath)) { | ||
content = await fs.readFile(cachePath) | ||
} else { | ||
content = await transformImage(url, options) | ||
await fs.writeFile(cachePath, content) | ||
} | ||
|
||
const file = url.pathname | ||
|
||
const fileHandle = this.emitFile({ | ||
name: `${path.basename(file, path.extname(file))}${ext}`, | ||
type: 'asset', | ||
source: content | ||
}) | ||
|
||
return dataToEsm(`__VITE_ASSET__${fileHandle}__`, globalConfig.build.rollupOptions) | ||
} | ||
|
||
} | ||
}) | ||
) | ||
|
||
const out = paths.reduce( | ||
(prev, { src, target }) => ({ | ||
...prev, | ||
[target.format]: src | ||
}), | ||
{} | ||
) | ||
|
||
return dataToEsm(out, { namedExports: false }) | ||
} | ||
}, | ||
|
||
renderChunk(code: string) { | ||
let match: RegExpExecArray | ||
let s: MagicString | ||
while ((match = assetUrlQuotedRE.exec(code))) { | ||
s = s || (s = new MagicString(code)) | ||
const [full, fileHandle, postfix = ''] = match | ||
|
||
const outputFilepath = config.base + this.getFileName(fileHandle) + postfix | ||
s.overwrite(match.index, match.index + full.length, JSON.stringify(outputFilepath)) | ||
} | ||
if (s) { | ||
return { | ||
code: s.toString(), | ||
map: config.build.sourcemap ? s.generateMap({ hires: true }) : null | ||
} | ||
} else { | ||
return null | ||
} | ||
} | ||
} | ||
} | ||
|
||
async function transformDev(sourceId: string, target: Target) { | ||
const id = transformId(sourceId, target) | ||
const info = await cacache.get.info(pluginOptions.cachePath, id) | ||
async function isCached(path: string) { | ||
try { | ||
const stat = await fs.stat(path) | ||
return stat.isFile() | ||
} catch { | ||
return false | ||
} | ||
|
||
if (info) { | ||
return { src: '/' + path.relative(config.root, info.path), target } | ||
} else { | ||
const content = await transformFile(sourceId, target) | ||
await cacache.put(pluginOptions.cachePath, id, content) | ||
const info = await cacache.get.info(pluginOptions.cachePath, id) | ||
} | ||
|
||
return { src: '/' + path.relative(config.root, info.path), target } | ||
} | ||
} | ||
|
||
async function transformBuild(sourceId: string, target: Target, ctx: PluginContext) { | ||
const id = transformId(sourceId, target) | ||
const info = await cacache.get.info(pluginOptions.cachePath, id) | ||
|
||
let content: Uint8Array | ||
if (info) { | ||
const { data } = await cacache.get(pluginOptions.cachePath, id) | ||
content = data | ||
} else { | ||
content = await transformFile(sourceId, target) | ||
await cacache.put(pluginOptions.cachePath, id, content) | ||
} | ||
function transformImage(url: URL, options: Options) { | ||
let pipeline = sharp(url.pathname) | ||
|
||
const file = cleanUrl(sourceId) | ||
const { search, hash } = new URL(sourceId) | ||
const postfix = (search || '') + (hash || '') | ||
|
||
const fileHandle = ctx.emitFile({ | ||
name: `${path.basename(file, path.extname(file))}.${target.format}`, | ||
type: 'asset', | ||
source: content | ||
}) | ||
|
||
return { src: `__IMAGESET__${fileHandle}__${postfix ? `${postfix}__` : ``}`, target } | ||
} | ||
|
||
async function transformFile(id: string, target: Target) { | ||
// load the file | ||
let img = sharp(id) | ||
// resize if necessary | ||
if (target.width || target.height) { | ||
img = img.resize(target) | ||
if (has(options, 'size', 'width', 'height')) { | ||
pipeline = pipeline.resize(options) | ||
} | ||
// transform | ||
if (target.transform) { | ||
img = target.transform(img, target.format) | ||
if (has(options, 'format')) { | ||
pipeline = pipeline.toFormat(options.format) | ||
} | ||
// transcode | ||
img = img[target.format](target) | ||
// return | ||
return img.toBuffer() | ||
} | ||
|
||
return pipeline.toBuffer() | ||
} | ||
|
||
function generateId(id: string) { | ||
return createHash('sha1').update(id).digest('hex').slice(0, 16) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { AvifOptions, GifOptions, JpegOptions, PngOptions, ResizeOptions, Sharp, TiffOptions, WebpOptions, FormatEnum } from "sharp"; | ||
import { Directive } from "./directives"; | ||
|
||
export interface Options extends ResizeOptions, JpegOptions, WebpOptions, PngOptions, AvifOptions, GifOptions, TiffOptions { | ||
format: keyof FormatEnum | ||
} | ||
|
||
export function buildOptions(url: URL, directives: Directive[]): Options { | ||
const parts = Array.from(url.searchParams.entries()).map(([dirName, argument]) => { | ||
const directive = directives.find(dir => dir.test(dirName, argument)) | ||
|
||
if (!directive) throw new Error('unknown directive ' + dirName) | ||
return directive.transform | ||
? directive.transform(dirName, argument) | ||
: { [dirName]: argument } | ||
}) | ||
return Object.assign({}, ...parts) | ||
} | ||
|
||
export function has(options: Options, ...keys: string[]) { | ||
return keys.some((key) => key in options) | ||
} |
Oops, something went wrong.