@artsy/img 0.0.1 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,27 @@
1
+ # v0.1.1 (Fri Dec 09 2022)
2
+
3
+ #### 🐛 Bug Fix
4
+
5
+ - fix: removes qs dependency; fixes gemini width/height modes [#8](https://github.com/artsy/img/pull/8) ([@dzucconi](https://github.com/dzucconi))
6
+
7
+ #### Authors: 1
8
+
9
+ - Damon ([@dzucconi](https://github.com/dzucconi))
10
+
11
+ ---
12
+
13
+ # v0.1.0 (Thu Dec 08 2022)
14
+
15
+ #### 🚀 Enhancement
16
+
17
+ - feat: optional services [#7](https://github.com/artsy/img/pull/7) ([@dzucconi](https://github.com/dzucconi))
18
+
19
+ #### Authors: 1
20
+
21
+ - Damon ([@dzucconi](https://github.com/dzucconi))
22
+
23
+ ---
24
+
1
25
  # v0.0.1 (Thu Dec 08 2022)
2
26
 
3
27
  #### 🐛 Bug Fix
package/dist/index.d.ts CHANGED
@@ -1 +1 @@
1
- export * from "./strategies";
1
+ export * from "./services";
package/dist/index.js CHANGED
@@ -14,4 +14,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("./strategies"), exports);
17
+ __exportStar(require("./services"), exports);
@@ -0,0 +1,5 @@
1
+ import { Exec } from "../services";
2
+ export type Gemini = {
3
+ endpoint: string;
4
+ };
5
+ export declare const gemini: (config: Gemini) => Exec;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.gemini = void 0;
4
+ const services_1 = require("../services");
5
+ const stringify_1 = require("../utils/stringify");
6
+ const gemini = (config) => {
7
+ return (mode, src, { width, height, quality = services_1.DEFAULT_QUALITY }) => {
8
+ if (!(0, services_1.validate)(mode, { width, height }))
9
+ return src;
10
+ let resizeTo;
11
+ switch (mode) {
12
+ case "crop": {
13
+ resizeTo = "fill";
14
+ }
15
+ case "resize": {
16
+ if (width && !height) {
17
+ resizeTo = "width";
18
+ break;
19
+ }
20
+ if (height && !width) {
21
+ resizeTo = "height";
22
+ break;
23
+ }
24
+ resizeTo = "fit";
25
+ }
26
+ }
27
+ const params = {
28
+ height,
29
+ resize_to: resizeTo,
30
+ src,
31
+ width,
32
+ quality,
33
+ };
34
+ const query = (0, stringify_1.stringify)(params);
35
+ return `${config.endpoint}?${query}`;
36
+ };
37
+ };
38
+ exports.gemini = gemini;
@@ -0,0 +1,6 @@
1
+ import { Exec } from "../services";
2
+ export type Imgix = {
3
+ endpoint: string;
4
+ token: string;
5
+ };
6
+ export declare const imgix: (config: Imgix) => Exec;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.imgix = void 0;
7
+ const md5_1 = __importDefault(require("md5"));
8
+ const services_1 = require("../services");
9
+ const stringify_1 = require("../utils/stringify");
10
+ const imgix = (config) => {
11
+ return (mode, src, { width, height, quality = services_1.DEFAULT_QUALITY }) => {
12
+ if (!(0, services_1.validate)(mode, { width, height }))
13
+ return src;
14
+ const params = {
15
+ fit: { crop: "crop", resize: "clip" }[mode],
16
+ height,
17
+ width,
18
+ quality,
19
+ auto: "format",
20
+ };
21
+ const path = `/${encodeURIComponent(src)}`;
22
+ const query = `?${(0, stringify_1.stringify)(params)}`;
23
+ const signature = (0, md5_1.default)(config.token + path + query);
24
+ return `${config.endpoint}${path}${query}&s=${signature}`;
25
+ };
26
+ };
27
+ exports.imgix = imgix;
@@ -0,0 +1,9 @@
1
+ import { Exec } from "../services";
2
+ export type Lambda = {
3
+ endpoint: string;
4
+ sources: {
5
+ source: string;
6
+ bucket: string;
7
+ }[];
8
+ };
9
+ export declare const lambda: (config: Lambda) => Exec;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.lambda = void 0;
4
+ const services_1 = require("../services");
5
+ const lambda = (config) => {
6
+ return (mode, src, { width, height, quality = services_1.DEFAULT_QUALITY }) => {
7
+ if (!(0, services_1.validate)(mode, { width, height }))
8
+ return src;
9
+ const source = config.sources.find((source) => {
10
+ return src.startsWith(source.source);
11
+ });
12
+ if (!source)
13
+ return src;
14
+ const params = {
15
+ bucket: source.bucket,
16
+ key: src.replace(`${source.source}/`, ""),
17
+ edits: {
18
+ resize: {
19
+ width,
20
+ height,
21
+ fit: { crop: "cover", resize: "inside" }[mode],
22
+ },
23
+ webp: { quality },
24
+ jpeg: { quality },
25
+ rotate: null,
26
+ },
27
+ };
28
+ const encoded = Buffer.from(JSON.stringify(params)).toString("base64");
29
+ return `${config.endpoint}/${encoded}`;
30
+ };
31
+ };
32
+ exports.lambda = lambda;
@@ -0,0 +1,27 @@
1
+ import { Gemini } from "./services/gemini";
2
+ import { Imgix } from "./services/imgix";
3
+ import { Lambda } from "./services/lambda";
4
+ export declare const SERVICES: readonly ["gemini", "imgix", "lambda"];
5
+ export declare const MODES: readonly ["resize", "crop"];
6
+ export declare const DEFAULT_QUALITY = 80;
7
+ export type Mode = typeof MODES[number];
8
+ export type Config = {
9
+ gemini?: Gemini;
10
+ imgix?: Imgix;
11
+ lambda?: Lambda;
12
+ };
13
+ export type Options = {
14
+ width?: number;
15
+ height?: number;
16
+ quality?: number;
17
+ };
18
+ export type Exec = (mode: Mode, src: string, options: Options) => string;
19
+ export type Service = {
20
+ exec: Exec;
21
+ };
22
+ export declare const validate: (mode: Mode, { width, height }: Options) => boolean;
23
+ /**
24
+ * Returns a list of configured services.
25
+ * All endpoints should *not* have trailing slashes.
26
+ */
27
+ export declare const configure: <T extends Config>(config: T) => Record<keyof T, Service>;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.configure = exports.validate = exports.DEFAULT_QUALITY = exports.MODES = exports.SERVICES = void 0;
4
+ const gemini_1 = require("./services/gemini");
5
+ const imgix_1 = require("./services/imgix");
6
+ const lambda_1 = require("./services/lambda");
7
+ exports.SERVICES = ["gemini", "imgix", "lambda"];
8
+ exports.MODES = ["resize", "crop"];
9
+ exports.DEFAULT_QUALITY = 80;
10
+ const validate = (mode, { width, height }) => {
11
+ if (mode === "crop" && (!width || !height)) {
12
+ console.warn("`crop`requires both `width` and `height`");
13
+ return false;
14
+ }
15
+ if (mode === "resize" && !width && !height) {
16
+ console.warn("`resize` requires either `width` or `height`");
17
+ return false;
18
+ }
19
+ if (width && width < 1) {
20
+ console.warn("`width` must be greater than `0`");
21
+ return false;
22
+ }
23
+ if (height && height < 1) {
24
+ console.warn("`height` must be greater than `0`");
25
+ return false;
26
+ }
27
+ return true;
28
+ };
29
+ exports.validate = validate;
30
+ /**
31
+ * Returns a list of configured services.
32
+ * All endpoints should *not* have trailing slashes.
33
+ */
34
+ const configure = (config) => {
35
+ const services = {};
36
+ if (config.gemini) {
37
+ services.gemini = { exec: (0, gemini_1.gemini)(config.gemini) };
38
+ }
39
+ if (config.imgix) {
40
+ services.imgix = { exec: (0, imgix_1.imgix)(config.imgix) };
41
+ }
42
+ if (config.lambda) {
43
+ services.lambda = { exec: (0, lambda_1.lambda)(config.lambda) };
44
+ }
45
+ return services;
46
+ };
47
+ exports.configure = configure;
@@ -0,0 +1 @@
1
+ export declare const stringify: (params: Record<string, string | number | undefined | null | boolean>) => string;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stringify = void 0;
4
+ const stringify = (params) => {
5
+ const keys = Object.keys(params)
6
+ // Alphabetize the keys
7
+ .sort((a, b) => a.localeCompare(b))
8
+ // Filter out null or undefined values
9
+ .filter((key) => params[key] != null);
10
+ return keys
11
+ .map((key) => {
12
+ return `${key}=${encodeURIComponent(`${params[key]}`)}`;
13
+ })
14
+ .join("&");
15
+ };
16
+ exports.stringify = stringify;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artsy/img",
3
- "version": "0.0.1",
3
+ "version": "0.1.1",
4
4
  "description": "Logic for constructing image proxy URLs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -30,7 +30,6 @@
30
30
  "test": "jest"
31
31
  },
32
32
  "dependencies": {
33
- "md5": "^2.3.0",
34
- "qs": "^6.11.0"
33
+ "md5": "^2.3.0"
35
34
  }
36
35
  }
@@ -1,6 +1,15 @@
1
- import { configure } from "../strategies";
1
+ import { configure } from "../services";
2
+
3
+ const EXAMPLE_SRC =
4
+ "https://d32dm0rphc51dk.cloudfront.net/MFFPXvpJSoGzggU8zujwBw/normalized.jpg";
2
5
 
3
6
  describe("strategies", () => {
7
+ it("only returns the services that are configured", () => {
8
+ const services = configure({ gemini: { endpoint: "https://example.com" } });
9
+
10
+ expect(Object.keys(services)).toEqual(["gemini"]);
11
+ });
12
+
4
13
  const { gemini, imgix, lambda } = configure({
5
14
  imgix: {
6
15
  endpoint: "https://example.imgix.net",
@@ -22,23 +31,34 @@ describe("strategies", () => {
22
31
 
23
32
  describe("gemini", () => {
24
33
  it("returns a resized url", () => {
25
- const img = gemini.exec(
26
- "resize",
27
- "https://d32dm0rphc51dk.cloudfront.net/MFFPXvpJSoGzggU8zujwBw/normalized.jpg",
28
- { width: 200, height: 200 }
29
- );
34
+ const img = gemini.exec("resize", EXAMPLE_SRC, {
35
+ width: 200,
36
+ height: 200,
37
+ });
30
38
 
31
39
  expect(img).toEqual(
32
40
  "https://d7hftxdivxxvm.cloudfront.net?height=200&quality=80&resize_to=fit&src=https%3A%2F%2Fd32dm0rphc51dk.cloudfront.net%2FMFFPXvpJSoGzggU8zujwBw%2Fnormalized.jpg&width=200"
33
41
  );
34
42
  });
35
43
 
36
- it("returns a cropped url", () => {
37
- const img = gemini.exec(
38
- "crop",
39
- "https://d32dm0rphc51dk.cloudfront.net/MFFPXvpJSoGzggU8zujwBw/normalized.jpg",
40
- { width: 200, height: 200 }
44
+ it("returns a resized url with only width specified", () => {
45
+ const img = gemini.exec("resize", EXAMPLE_SRC, { width: 200 });
46
+
47
+ expect(img).toEqual(
48
+ "https://d7hftxdivxxvm.cloudfront.net?quality=80&resize_to=width&src=https%3A%2F%2Fd32dm0rphc51dk.cloudfront.net%2FMFFPXvpJSoGzggU8zujwBw%2Fnormalized.jpg&width=200"
49
+ );
50
+ });
51
+
52
+ it("returns a resized url with only height specified", () => {
53
+ const img = gemini.exec("resize", EXAMPLE_SRC, { height: 200 });
54
+
55
+ expect(img).toEqual(
56
+ "https://d7hftxdivxxvm.cloudfront.net?height=200&quality=80&resize_to=height&src=https%3A%2F%2Fd32dm0rphc51dk.cloudfront.net%2FMFFPXvpJSoGzggU8zujwBw%2Fnormalized.jpg"
41
57
  );
58
+ });
59
+
60
+ it("returns a cropped url", () => {
61
+ const img = gemini.exec("crop", EXAMPLE_SRC, { width: 200, height: 200 });
42
62
 
43
63
  expect(img).toEqual(
44
64
  "https://d7hftxdivxxvm.cloudfront.net?height=200&quality=80&resize_to=fit&src=https%3A%2F%2Fd32dm0rphc51dk.cloudfront.net%2FMFFPXvpJSoGzggU8zujwBw%2Fnormalized.jpg&width=200"
@@ -48,37 +68,31 @@ describe("strategies", () => {
48
68
 
49
69
  describe("imgix", () => {
50
70
  it("returns a resized url", () => {
51
- const img = imgix.exec(
52
- "resize",
53
- "https://d32dm0rphc51dk.cloudfront.net/MFFPXvpJSoGzggU8zujwBw/normalized.jpg",
54
- { width: 200, height: 200 }
55
- );
71
+ const img = imgix.exec("resize", EXAMPLE_SRC, {
72
+ width: 200,
73
+ height: 200,
74
+ });
56
75
 
57
76
  expect(img).toEqual(
58
- "https://example.imgix.net/https%3A%2F%2Fd32dm0rphc51dk.cloudfront.net%2FMFFPXvpJSoGzggU8zujwBw%2Fnormalized.jpg?fit=clip&height=200&width=200&quality=80&auto=format&s=5cce72615e263db4c22e027d2ae73cd1"
77
+ "https://example.imgix.net/https%3A%2F%2Fd32dm0rphc51dk.cloudfront.net%2FMFFPXvpJSoGzggU8zujwBw%2Fnormalized.jpg?auto=format&fit=clip&height=200&quality=80&width=200&s=1d931e11fbfb846dbe5f1ffa895017d9"
59
78
  );
60
79
  });
61
80
 
62
81
  it("returns a cropped url", () => {
63
- const img = imgix.exec(
64
- "crop",
65
- "https://d32dm0rphc51dk.cloudfront.net/MFFPXvpJSoGzggU8zujwBw/normalized.jpg",
66
- { width: 200, height: 200 }
67
- );
82
+ const img = imgix.exec("crop", EXAMPLE_SRC, { width: 200, height: 200 });
68
83
 
69
84
  expect(img).toEqual(
70
- "https://example.imgix.net/https%3A%2F%2Fd32dm0rphc51dk.cloudfront.net%2FMFFPXvpJSoGzggU8zujwBw%2Fnormalized.jpg?fit=crop&height=200&width=200&quality=80&auto=format&s=3fd908feecc9fecb39aebe909071c362"
85
+ "https://example.imgix.net/https%3A%2F%2Fd32dm0rphc51dk.cloudfront.net%2FMFFPXvpJSoGzggU8zujwBw%2Fnormalized.jpg?auto=format&fit=crop&height=200&quality=80&width=200&s=fd511845304eb4a5cad016b52aee939b"
71
86
  );
72
87
  });
73
88
  });
74
89
 
75
90
  describe("lambda", () => {
76
91
  it("returns a resized url", () => {
77
- const img = lambda.exec(
78
- "resize",
79
- "https://d32dm0rphc51dk.cloudfront.net/MFFPXvpJSoGzggU8zujwBw/normalized.jpg",
80
- { width: 200, height: 200 }
81
- );
92
+ const img = lambda.exec("resize", EXAMPLE_SRC, {
93
+ width: 200,
94
+ height: 200,
95
+ });
82
96
 
83
97
  expect(img).toEqual(
84
98
  "https://d1j88w5k23s1nr.cloudfront.net/eyJidWNrZXQiOiJhcnRzeS1tZWRpYS1hc3NldHMiLCJrZXkiOiJNRkZQWHZwSlNvR3pnZ1U4enVqd0J3L25vcm1hbGl6ZWQuanBnIiwiZWRpdHMiOnsicmVzaXplIjp7IndpZHRoIjoyMDAsImhlaWdodCI6MjAwLCJmaXQiOiJpbnNpZGUifSwid2VicCI6eyJxdWFsaXR5Ijo4MH0sImpwZWciOnsicXVhbGl0eSI6ODB9LCJyb3RhdGUiOm51bGx9fQ=="
@@ -86,11 +100,7 @@ describe("strategies", () => {
86
100
  });
87
101
 
88
102
  it("returns a cropped url", () => {
89
- const img = lambda.exec(
90
- "crop",
91
- "https://d32dm0rphc51dk.cloudfront.net/MFFPXvpJSoGzggU8zujwBw/normalized.jpg",
92
- { width: 200, height: 200 }
93
- );
103
+ const img = lambda.exec("crop", EXAMPLE_SRC, { width: 200, height: 200 });
94
104
 
95
105
  expect(img).toEqual(
96
106
  "https://d1j88w5k23s1nr.cloudfront.net/eyJidWNrZXQiOiJhcnRzeS1tZWRpYS1hc3NldHMiLCJrZXkiOiJNRkZQWHZwSlNvR3pnZ1U4enVqd0J3L25vcm1hbGl6ZWQuanBnIiwiZWRpdHMiOnsicmVzaXplIjp7IndpZHRoIjoyMDAsImhlaWdodCI6MjAwLCJmaXQiOiJjb3ZlciJ9LCJ3ZWJwIjp7InF1YWxpdHkiOjgwfSwianBlZyI6eyJxdWFsaXR5Ijo4MH0sInJvdGF0ZSI6bnVsbH19"
package/src/index.ts CHANGED
@@ -1 +1 @@
1
- export * from "./strategies";
1
+ export * from "./services";
@@ -0,0 +1,46 @@
1
+ import { DEFAULT_QUALITY, Exec, validate } from "../services";
2
+ import { stringify } from "../utils/stringify";
3
+
4
+ export type Gemini = {
5
+ endpoint: string;
6
+ };
7
+
8
+ export const gemini = (config: Gemini): Exec => {
9
+ return (mode, src, { width, height, quality = DEFAULT_QUALITY }) => {
10
+ if (!validate(mode, { width, height })) return src;
11
+
12
+ let resizeTo: "width" | "height" | "fit" | "fill";
13
+
14
+ switch (mode) {
15
+ case "crop": {
16
+ resizeTo = "fill";
17
+ }
18
+
19
+ case "resize": {
20
+ if (width && !height) {
21
+ resizeTo = "width";
22
+ break;
23
+ }
24
+
25
+ if (height && !width) {
26
+ resizeTo = "height";
27
+ break;
28
+ }
29
+
30
+ resizeTo = "fit";
31
+ }
32
+ }
33
+
34
+ const params = {
35
+ height,
36
+ resize_to: resizeTo,
37
+ src,
38
+ width,
39
+ quality,
40
+ };
41
+
42
+ const query = stringify(params);
43
+
44
+ return `${config.endpoint}?${query}`;
45
+ };
46
+ };
@@ -0,0 +1,28 @@
1
+ import md5 from "md5";
2
+ import { DEFAULT_QUALITY, Exec, validate } from "../services";
3
+ import { stringify } from "../utils/stringify";
4
+
5
+ export type Imgix = {
6
+ endpoint: string;
7
+ token: string;
8
+ };
9
+
10
+ export const imgix = (config: Imgix): Exec => {
11
+ return (mode, src, { width, height, quality = DEFAULT_QUALITY }) => {
12
+ if (!validate(mode, { width, height })) return src;
13
+
14
+ const params = {
15
+ fit: { crop: "crop", resize: "clip" }[mode],
16
+ height,
17
+ width,
18
+ quality,
19
+ auto: "format",
20
+ };
21
+
22
+ const path = `/${encodeURIComponent(src)}`;
23
+ const query = `?${stringify(params)}`;
24
+ const signature = md5(config.token + path + query);
25
+
26
+ return `${config.endpoint}${path}${query}&s=${signature}`;
27
+ };
28
+ };
@@ -0,0 +1,40 @@
1
+ import { DEFAULT_QUALITY, Exec, validate } from "../services";
2
+
3
+ export type Lambda = {
4
+ endpoint: string;
5
+ sources: {
6
+ source: string;
7
+ bucket: string;
8
+ }[];
9
+ };
10
+
11
+ export const lambda = (config: Lambda): Exec => {
12
+ return (mode, src, { width, height, quality = DEFAULT_QUALITY }) => {
13
+ if (!validate(mode, { width, height })) return src;
14
+
15
+ const source = config.sources.find((source) => {
16
+ return src.startsWith(source.source);
17
+ });
18
+
19
+ if (!source) return src;
20
+
21
+ const params = {
22
+ bucket: source.bucket,
23
+ key: src.replace(`${source.source}/`, ""),
24
+ edits: {
25
+ resize: {
26
+ width,
27
+ height,
28
+ fit: { crop: "cover", resize: "inside" }[mode],
29
+ },
30
+ webp: { quality },
31
+ jpeg: { quality },
32
+ rotate: null,
33
+ },
34
+ };
35
+
36
+ const encoded = Buffer.from(JSON.stringify(params)).toString("base64");
37
+
38
+ return `${config.endpoint}/${encoded}`;
39
+ };
40
+ };
@@ -0,0 +1,73 @@
1
+ import { Gemini, gemini } from "./services/gemini";
2
+ import { imgix, Imgix } from "./services/imgix";
3
+ import { lambda, Lambda } from "./services/lambda";
4
+
5
+ export const SERVICES = ["gemini", "imgix", "lambda"] as const;
6
+
7
+ export const MODES = ["resize", "crop"] as const;
8
+
9
+ export const DEFAULT_QUALITY = 80;
10
+
11
+ export type Mode = typeof MODES[number];
12
+
13
+ export type Config = {
14
+ gemini?: Gemini;
15
+ imgix?: Imgix;
16
+ lambda?: Lambda;
17
+ };
18
+
19
+ export type Options = {
20
+ width?: number;
21
+ height?: number;
22
+ quality?: number;
23
+ };
24
+
25
+ export type Exec = (mode: Mode, src: string, options: Options) => string;
26
+
27
+ export type Service = { exec: Exec };
28
+
29
+ export const validate = (mode: Mode, { width, height }: Options) => {
30
+ if (mode === "crop" && (!width || !height)) {
31
+ console.warn("`crop`requires both `width` and `height`");
32
+ return false;
33
+ }
34
+
35
+ if (mode === "resize" && !width && !height) {
36
+ console.warn("`resize` requires either `width` or `height`");
37
+ return false;
38
+ }
39
+
40
+ if (width && width < 1) {
41
+ console.warn("`width` must be greater than `0`");
42
+ return false;
43
+ }
44
+
45
+ if (height && height < 1) {
46
+ console.warn("`height` must be greater than `0`");
47
+ return false;
48
+ }
49
+
50
+ return true;
51
+ };
52
+
53
+ /**
54
+ * Returns a list of configured services.
55
+ * All endpoints should *not* have trailing slashes.
56
+ */
57
+ export const configure = <T extends Config>(config: T) => {
58
+ const services = {} as Record<keyof T, Service>;
59
+
60
+ if (config.gemini) {
61
+ services.gemini = { exec: gemini(config.gemini) };
62
+ }
63
+
64
+ if (config.imgix) {
65
+ services.imgix = { exec: imgix(config.imgix) };
66
+ }
67
+
68
+ if (config.lambda) {
69
+ services.lambda = { exec: lambda(config.lambda) };
70
+ }
71
+
72
+ return services;
73
+ };
@@ -0,0 +1,15 @@
1
+ export const stringify = (
2
+ params: Record<string, string | number | undefined | null | boolean>
3
+ ): string => {
4
+ const keys = Object.keys(params)
5
+ // Alphabetize the keys
6
+ .sort((a, b) => a.localeCompare(b))
7
+ // Filter out null or undefined values
8
+ .filter((key) => params[key] != null);
9
+
10
+ return keys
11
+ .map((key) => {
12
+ return `${key}=${encodeURIComponent(`${params[key]}`)}`;
13
+ })
14
+ .join("&");
15
+ };
@@ -1,39 +0,0 @@
1
- export declare const SERVICES: readonly ["gemini", "imgix", "lambda"];
2
- export declare const MODES: readonly ["resize", "crop"];
3
- export declare const DEFAULT_QUALITY = 80;
4
- type Mode = typeof MODES[number];
5
- /**
6
- * All endpoints should *not* have trailing slashes
7
- */
8
- type Config = {
9
- gemini: {
10
- endpoint: string;
11
- };
12
- imgix: {
13
- endpoint: string;
14
- token: string;
15
- };
16
- lambda: {
17
- endpoint: string;
18
- sources: {
19
- source: string;
20
- bucket: string;
21
- }[];
22
- };
23
- };
24
- type Options = {
25
- width?: number;
26
- height?: number;
27
- quality?: number;
28
- };
29
- type Exec = (mode: Mode, src: string, options: Options) => string;
30
- type Strategy = {
31
- exec: Exec;
32
- };
33
- export declare const validate: (mode: Mode, { width, height }: Options) => boolean;
34
- export declare const configure: (config: Config) => {
35
- gemini: Strategy;
36
- imgix: Strategy;
37
- lambda: Strategy;
38
- };
39
- export {};
@@ -1,115 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.configure = exports.validate = exports.DEFAULT_QUALITY = exports.MODES = exports.SERVICES = void 0;
7
- const md5_1 = __importDefault(require("md5"));
8
- const qs_1 = require("qs");
9
- exports.SERVICES = ["gemini", "imgix", "lambda"];
10
- exports.MODES = ["resize", "crop"];
11
- exports.DEFAULT_QUALITY = 80;
12
- const validate = (mode, { width, height }) => {
13
- if (mode === "crop" && (!width || !height)) {
14
- console.warn("`crop`requires both `width` and `height`");
15
- return false;
16
- }
17
- if (mode === "resize" && !width && !height) {
18
- console.warn("`resize` requires either `width` or `height`");
19
- return false;
20
- }
21
- if (width && width < 1) {
22
- console.warn("`width` must be greater than `0`");
23
- return false;
24
- }
25
- if (height && height < 1) {
26
- console.warn("`height` must be greater than `0`");
27
- return false;
28
- }
29
- return true;
30
- };
31
- exports.validate = validate;
32
- const configure = (config) => {
33
- const strategies = {
34
- gemini: {
35
- exec: (mode, src, { width, height, quality = exports.DEFAULT_QUALITY }) => {
36
- if (!(0, exports.validate)(mode, { width, height }))
37
- return src;
38
- let resizeTo;
39
- switch (mode) {
40
- case "crop": {
41
- resizeTo = "fill";
42
- }
43
- case "resize": {
44
- if (width && !height) {
45
- resizeTo = "width";
46
- }
47
- if (height && !width) {
48
- resizeTo = "height";
49
- }
50
- resizeTo = "fit";
51
- }
52
- }
53
- const params = {
54
- height,
55
- resize_to: resizeTo,
56
- src,
57
- width,
58
- quality,
59
- };
60
- const query = (0, qs_1.stringify)(params, {
61
- arrayFormat: "brackets",
62
- skipNulls: true,
63
- sort: (a, b) => a.localeCompare(b),
64
- });
65
- return `${config.gemini.endpoint}?${query}`;
66
- },
67
- },
68
- imgix: {
69
- exec: (mode, src, { width, height, quality = exports.DEFAULT_QUALITY }) => {
70
- if (!(0, exports.validate)(mode, { width, height }))
71
- return src;
72
- const params = {
73
- fit: { crop: "crop", resize: "clip" }[mode],
74
- height,
75
- width,
76
- quality,
77
- auto: "format",
78
- };
79
- const path = `/${encodeURIComponent(src)}`;
80
- const query = `?${(0, qs_1.stringify)(params)}`;
81
- const signature = (0, md5_1.default)(config.imgix.token + path + query);
82
- return `${config.imgix.endpoint}${path}${query}&s=${signature}`;
83
- },
84
- },
85
- lambda: {
86
- exec: (mode, src, { width, height, quality = exports.DEFAULT_QUALITY }) => {
87
- if (!(0, exports.validate)(mode, { width, height }))
88
- return src;
89
- const source = config.lambda.sources.find((source) => {
90
- return src.startsWith(source.source);
91
- });
92
- if (!source)
93
- return src;
94
- const params = {
95
- bucket: source.bucket,
96
- key: src.replace(`${source.source}/`, ""),
97
- edits: {
98
- resize: {
99
- width,
100
- height,
101
- fit: { crop: "cover", resize: "inside" }[mode],
102
- },
103
- webp: { quality },
104
- jpeg: { quality },
105
- rotate: null,
106
- },
107
- };
108
- const encoded = Buffer.from(JSON.stringify(params)).toString("base64");
109
- return `${config.lambda.endpoint}/${encoded}`;
110
- },
111
- },
112
- };
113
- return strategies;
114
- };
115
- exports.configure = configure;
package/src/strategies.ts DELETED
@@ -1,167 +0,0 @@
1
- import md5 from "md5";
2
- import { stringify } from "qs";
3
-
4
- export const SERVICES = ["gemini", "imgix", "lambda"] as const;
5
-
6
- export const MODES = ["resize", "crop"] as const;
7
-
8
- export const DEFAULT_QUALITY = 80;
9
-
10
- type Service = typeof SERVICES[number];
11
-
12
- type Mode = typeof MODES[number];
13
-
14
- /**
15
- * All endpoints should *not* have trailing slashes
16
- */
17
- type Config = {
18
- gemini: {
19
- endpoint: string;
20
- };
21
- imgix: {
22
- endpoint: string;
23
- token: string;
24
- };
25
- lambda: {
26
- endpoint: string;
27
- sources: {
28
- source: string;
29
- bucket: string;
30
- }[];
31
- };
32
- };
33
-
34
- type Options = {
35
- width?: number;
36
- height?: number;
37
- quality?: number;
38
- };
39
-
40
- type Exec = (mode: Mode, src: string, options: Options) => string;
41
-
42
- type Strategy = { exec: Exec };
43
-
44
- export const validate = (mode: Mode, { width, height }: Options) => {
45
- if (mode === "crop" && (!width || !height)) {
46
- console.warn("`crop`requires both `width` and `height`");
47
- return false;
48
- }
49
-
50
- if (mode === "resize" && !width && !height) {
51
- console.warn("`resize` requires either `width` or `height`");
52
- return false;
53
- }
54
-
55
- if (width && width < 1) {
56
- console.warn("`width` must be greater than `0`");
57
- return false;
58
- }
59
-
60
- if (height && height < 1) {
61
- console.warn("`height` must be greater than `0`");
62
- return false;
63
- }
64
-
65
- return true;
66
- };
67
-
68
- export const configure = (config: Config) => {
69
- const strategies: {
70
- [key in Service]: Strategy;
71
- } = {
72
- gemini: {
73
- exec: (mode, src, { width, height, quality = DEFAULT_QUALITY }) => {
74
- if (!validate(mode, { width, height })) return src;
75
-
76
- let resizeTo: "width" | "height" | "fit" | "fill";
77
-
78
- switch (mode) {
79
- case "crop": {
80
- resizeTo = "fill";
81
- }
82
-
83
- case "resize": {
84
- if (width && !height) {
85
- resizeTo = "width";
86
- }
87
-
88
- if (height && !width) {
89
- resizeTo = "height";
90
- }
91
-
92
- resizeTo = "fit";
93
- }
94
- }
95
-
96
- const params = {
97
- height,
98
- resize_to: resizeTo,
99
- src,
100
- width,
101
- quality,
102
- };
103
-
104
- const query = stringify(params, {
105
- arrayFormat: "brackets",
106
- skipNulls: true,
107
- sort: (a, b) => a.localeCompare(b),
108
- });
109
-
110
- return `${config.gemini.endpoint}?${query}`;
111
- },
112
- },
113
-
114
- imgix: {
115
- exec: (mode, src, { width, height, quality = DEFAULT_QUALITY }) => {
116
- if (!validate(mode, { width, height })) return src;
117
-
118
- const params = {
119
- fit: { crop: "crop", resize: "clip" }[mode],
120
- height,
121
- width,
122
- quality,
123
- auto: "format",
124
- };
125
-
126
- const path = `/${encodeURIComponent(src)}`;
127
- const query = `?${stringify(params)}`;
128
- const signature = md5(config.imgix.token + path + query);
129
-
130
- return `${config.imgix.endpoint}${path}${query}&s=${signature}`;
131
- },
132
- },
133
-
134
- lambda: {
135
- exec: (mode, src, { width, height, quality = DEFAULT_QUALITY }) => {
136
- if (!validate(mode, { width, height })) return src;
137
-
138
- const source = config.lambda.sources.find((source) => {
139
- return src.startsWith(source.source);
140
- });
141
-
142
- if (!source) return src;
143
-
144
- const params = {
145
- bucket: source.bucket,
146
- key: src.replace(`${source.source}/`, ""),
147
- edits: {
148
- resize: {
149
- width,
150
- height,
151
- fit: { crop: "cover", resize: "inside" }[mode],
152
- },
153
- webp: { quality },
154
- jpeg: { quality },
155
- rotate: null,
156
- },
157
- };
158
-
159
- const encoded = Buffer.from(JSON.stringify(params)).toString("base64");
160
-
161
- return `${config.lambda.endpoint}/${encoded}`;
162
- },
163
- },
164
- };
165
-
166
- return strategies;
167
- };