Skip to content

Commit

Permalink
feat: add width/w and height/h query parameters to resize image
Browse files Browse the repository at this point in the history
  • Loading branch information
RAX7 authored Sep 15, 2022
1 parent a57fcdf commit 52ee1c8
Show file tree
Hide file tree
Showing 7 changed files with 308 additions and 14 deletions.
7 changes: 7 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ const {
* @typedef {InferDefaultType<T> | undefined} BasicTransformerOptions
*/

/**
* @typedef {Object} ResizeOptions
* @property {number} [width]
* @property {number} [height]
* @property {boolean} [enabled]
*/

/**
* @template T
* @callback BasicTransformerImplementation
Expand Down
102 changes: 89 additions & 13 deletions src/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@ const { isAbsoluteURL } = require("./utils.js");
/** @typedef {import("webpack").Compilation} Compilation */
/** @typedef {import("./utils").WorkerResult} WorkerResult */

/**
* @template T
* @typedef {import("./index").Minimizer<T>} Minimizer<T>
*/

/**
* @template T
* @typedef {import("./index").Generator<T>} Generator<T>
*/

/**
* @template T
* @typedef {Object} LoaderOptions<T>
* @property {string} [severityError] Allows to choose how errors are displayed.
* @property {import("./index").Minimizer<T> | import("./index").Minimizer<T>[]} [minimizer]
* @property {import("./index").Generator<T>[]} [generator]
* @property {Minimizer<T> | Minimizer<T>[]} [minimizer]
* @property {Generator<T>[]} [generator]
*/

// Workaround - https://github.com/webpack-contrib/image-minimizer-webpack-plugin/issues/341
Expand All @@ -31,6 +41,49 @@ function changeResource(loaderContext, isAbsolute, output, query) {
loaderContext.resourceQuery = query;
}

/**
* @template T
* @param {Minimizer<T>[]} transformers
* @param {string | null} widthQuery
* @param {string | null} heightQuery
* @return {Minimizer<T>[]}
*/
function processSizeQuery(transformers, widthQuery, heightQuery) {
return transformers.map((transformer) => {
const minimizer = { ...transformer };

const minimizerOptions =
/** @type { import("./index").BasicTransformerOptions<T> & { resize?: import("./index").ResizeOptions }} */
// @ts-ignore
({ ...minimizer.options });

minimizerOptions.resize = { ...minimizerOptions?.resize };
minimizer.options = minimizerOptions;

if (widthQuery === "auto") {
delete minimizerOptions.resize.width;
} else if (widthQuery) {
const width = Number.parseInt(widthQuery, 10);

if (Number.isFinite(width) && width > 0) {
minimizerOptions.resize.width = width;
}
}

if (heightQuery === "auto") {
delete minimizerOptions.resize.height;
} else if (heightQuery) {
const height = Number.parseInt(heightQuery, 10);

if (Number.isFinite(height) && height > 0) {
minimizerOptions.resize.height = height;
}
}

return minimizer;
});
}

/**
* @template T
* @this {import("webpack").LoaderContext<LoaderOptions<T>>}
Expand Down Expand Up @@ -62,12 +115,16 @@ async function loader(content) {
}

let transformer = minimizer;
let parsedQuery;

if (this.resourceQuery.length > 0) {
parsedQuery = new URLSearchParams(this.resourceQuery);
const parsedQuery =
this.resourceQuery.length > 0
? new URLSearchParams(this.resourceQuery)
: null;

if (parsedQuery.has("as")) {
if (parsedQuery) {
const presetName = parsedQuery.get("as");

if (presetName) {
if (!generator) {
callback(
new Error(
Expand All @@ -78,8 +135,7 @@ async function loader(content) {
return;
}

const as = parsedQuery.get("as");
const presets = generator.filter((item) => item.preset === as);
const presets = generator.filter((item) => item.preset === presetName);

if (presets.length > 1) {
callback(
Expand All @@ -93,7 +149,9 @@ async function loader(content) {

if (presets.length === 0) {
callback(
new Error(`Can't find '${as}' preset in the 'generator' option`)
new Error(
`Can't find '${presetName}' preset in the 'generator' option`
)
);

return;
Expand All @@ -109,6 +167,23 @@ async function loader(content) {
return;
}

if (parsedQuery) {
const widthQuery = parsedQuery.get("width") ?? parsedQuery.get("w");
const heightQuery = parsedQuery.get("height") ?? parsedQuery.get("h");

if (widthQuery || heightQuery) {
if (Array.isArray(transformer)) {
transformer = processSizeQuery(transformer, widthQuery, heightQuery);
} else {
[transformer] = processSizeQuery(
[transformer],
widthQuery,
heightQuery
);
}
}
}

const isAbsolute = isAbsoluteURL(this.resourcePath);
const filename = isAbsolute
? this.resourcePath
Expand Down Expand Up @@ -160,11 +235,12 @@ async function loader(content) {

if (parsedQuery) {
// Remove query param from the bundle due we need that only for bundle purposes
const stringifiedParsedQuery = parsedQuery.toString();
["as", "width", "w", "height", "h"].forEach((key) =>
parsedQuery.delete(key)
);

query =
stringifiedParsedQuery.length > 0 ? `?${stringifiedParsedQuery}` : "";
parsedQuery.delete("as");
query = parsedQuery.toString();
query = query.length > 0 ? `?${query}` : "";
}

// Old approach for `file-loader` and other old loaders
Expand Down
1 change: 1 addition & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,7 @@ async function squooshGenerate(original, minifyOptions) {

const { binary, extension } = await Object.values(image.encodedWith)[0];
const { width, height } = (await image.decoded).bitmap;

const { dir: fileDir, name: fileName } = path.parse(original.filename);
const filename = path.join(fileDir, `${fileName}.${extension}`);

Expand Down
15 changes: 15 additions & 0 deletions test/fixtures/generator-and-minimizer-resize-query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
require("./loader-test.png?width=100");
require("./loader-test.png?w=150");
require("./loader-test.png?height=200");
require("./loader-test.png?h=250");
require("./loader-test.png?width=300&height=auto");
require("./loader-test.png?width=auto&height=320");
require("./loader-test.png?width=350&height=350");

require("./loader-test.png?as=webp&width=100");
require("./loader-test.png?as=webp&w=150");
require("./loader-test.png?as=webp&height=200");
require("./loader-test.png?as=webp&h=250");
require("./loader-test.png?as=webp&width=300&height=auto");
require("./loader-test.png?as=webp&width=auto&height=320");
require("./loader-test.png?as=webp&width=350&height=350");
167 changes: 167 additions & 0 deletions test/resize-query.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import path from "path";
import { promisify } from "util";
import fileType from "file-type";
import imageSize from "image-size";
import ImageMinimizerPlugin from "../src";

import { runWebpack, fixturesPath } from "./helpers";

jest.setTimeout(20000);

describe("resize query", () => {
it("should generate and resize with resize options", async () => {
const stats = await runWebpack({
entry: path.join(
fixturesPath,
"./generator-and-minimizer-resize-query.js"
),
imageminLoaderOptions: {
minimizer: {
implementation: ImageMinimizerPlugin.sharpMinify,
filename: "[name]-[width]x[height][ext]",
options: {
resize: {
width: 400,
height: 400,
},
encodeOptions: {
png: {},
},
},
},
generator: [
{
preset: "webp",
implementation: ImageMinimizerPlugin.sharpGenerate,
filename: "[name]-[width]x[height][ext]",
options: {
resize: {
width: 400,
height: 400,
},
encodeOptions: {
webp: {},
},
},
},
],
},
});

const { compilation } = stats;
const { warnings, errors } = compilation;
const sizeOf = promisify(imageSize);

const assetsList = [
// asset path, width, height, mime regExp
["./loader-test-100x400.png", 100, 400, /image\/png/i],
["./loader-test-150x400.png", 150, 400, /image\/png/i],
["./loader-test-400x200.png", 400, 200, /image\/png/i],
["./loader-test-400x250.png", 400, 250, /image\/png/i],
["./loader-test-300x300.png", 300, 300, /image\/png/i],
["./loader-test-320x320.png", 320, 320, /image\/png/i],
["./loader-test-350x350.png", 350, 350, /image\/png/i],

["./loader-test-100x400.webp", 100, 400, /image\/webp/i],
["./loader-test-150x400.webp", 150, 400, /image\/webp/i],
["./loader-test-400x200.webp", 400, 200, /image\/webp/i],
["./loader-test-400x250.webp", 400, 250, /image\/webp/i],
["./loader-test-300x300.webp", 300, 300, /image\/webp/i],
["./loader-test-320x320.webp", 320, 320, /image\/webp/i],
["./loader-test-350x350.webp", 350, 350, /image\/webp/i],
];

for (const [assetPath, width, height, mimeRegExp] of assetsList) {
const transformedAsset = path.resolve(
__dirname,
compilation.options.output.path,
assetPath
);

// eslint-disable-next-line no-await-in-loop
const transformedExt = await fileType.fromFile(transformedAsset);
// eslint-disable-next-line no-await-in-loop
const dimensions = await sizeOf(transformedAsset);

expect(dimensions.width).toBe(width);
expect(dimensions.height).toBe(height);
expect(mimeRegExp.test(transformedExt.mime)).toBe(true);
expect(warnings).toHaveLength(0);
expect(errors).toHaveLength(0);
}
});

it("should generate and resize without resize options", async () => {
const stats = await runWebpack({
entry: path.join(
fixturesPath,
"./generator-and-minimizer-resize-query.js"
),
imageminLoaderOptions: {
minimizer: {
implementation: ImageMinimizerPlugin.sharpMinify,
filename: "[name]-[width]x[height][ext]",
options: {
encodeOptions: {
png: {},
},
},
},
generator: [
{
preset: "webp",
implementation: ImageMinimizerPlugin.sharpGenerate,
filename: "[name]-[width]x[height][ext]",
options: {
encodeOptions: {
webp: {},
},
},
},
],
},
});

const { compilation } = stats;
const { warnings, errors } = compilation;
const sizeOf = promisify(imageSize);

const assetsList = [
// asset path, width, height, mime regExp
["./loader-test-100x100.png", 100, 100, /image\/png/i],
["./loader-test-150x150.png", 150, 150, /image\/png/i],
["./loader-test-200x200.png", 200, 200, /image\/png/i],
["./loader-test-250x250.png", 250, 250, /image\/png/i],
["./loader-test-300x300.png", 300, 300, /image\/png/i],
["./loader-test-320x320.png", 320, 320, /image\/png/i],
["./loader-test-350x350.png", 350, 350, /image\/png/i],

["./loader-test-100x100.webp", 100, 100, /image\/webp/i],
["./loader-test-150x150.webp", 150, 150, /image\/webp/i],
["./loader-test-200x200.webp", 200, 200, /image\/webp/i],
["./loader-test-250x250.webp", 250, 250, /image\/webp/i],
["./loader-test-300x300.webp", 300, 300, /image\/webp/i],
["./loader-test-320x320.webp", 320, 320, /image\/webp/i],
["./loader-test-350x350.webp", 350, 350, /image\/webp/i],
];

for (const [assetPath, width, height, mimeRegExp] of assetsList) {
const transformedAsset = path.resolve(
__dirname,
compilation.options.output.path,
assetPath
);

// eslint-disable-next-line no-await-in-loop
const transformedExt = await fileType.fromFile(transformedAsset);
// eslint-disable-next-line no-await-in-loop
const dimensions = await sizeOf(transformedAsset);

expect(dimensions.width).toBe(width);
expect(dimensions.height).toBe(height);
expect(mimeRegExp.test(transformedExt.mime)).toBe(true);
expect(warnings).toHaveLength(0);
expect(errors).toHaveLength(0);
}
});
});
12 changes: 12 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ export = ImageMinimizerPlugin;
* @template T
* @typedef {InferDefaultType<T> | undefined} BasicTransformerOptions
*/
/**
* @typedef {Object} ResizeOptions
* @property {number} [width]
* @property {number} [height]
* @property {boolean} [enabled]
*/
/**
* @template T
* @callback BasicTransformerImplementation
Expand Down Expand Up @@ -187,6 +193,7 @@ declare namespace ImageMinimizerPlugin {
CustomOptions,
InferDefaultType,
BasicTransformerOptions,
ResizeOptions,
BasicTransformerImplementation,
BasicTransformerHelpers,
TransformerFunction,
Expand Down Expand Up @@ -303,6 +310,11 @@ type CustomOptions = {
};
type InferDefaultType<T> = T extends infer U ? U : CustomOptions;
type BasicTransformerOptions<T> = InferDefaultType<T> | undefined;
type ResizeOptions = {
width?: number | undefined;
height?: number | undefined;
enabled?: boolean | undefined;
};
type BasicTransformerImplementation<T> = (
original: WorkerResult,
options?: BasicTransformerOptions<T>
Expand Down
Loading

0 comments on commit 52ee1c8

Please sign in to comment.