Skip to content

Commit a8510be

Browse files
authored
Use pixel package (#90)
Uses pixel library as wrapper for the Next.js image optimization API
1 parent 7a907f5 commit a8510be

File tree

9 files changed

+98
-95
lines changed

9 files changed

+98
-95
lines changed

.github/workflows/CI.yml

+8-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ on:
99
jobs:
1010
build:
1111
runs-on: ubuntu-latest
12-
container: registry.gitlab.com/dealmore/dealmore-build-images/lambda:nodejs14.x
13-
12+
container: public.ecr.aws/sam/build-nodejs14.x:latest
1413
steps:
14+
- name: Install yarn
15+
run: npm install --global [email protected]
16+
1517
- uses: actions/checkout@v2
1618

1719
- name: Cache
@@ -38,7 +40,7 @@ jobs:
3840

3941
test-integration:
4042
runs-on: ubuntu-latest
41-
container: registry.gitlab.com/dealmore/dealmore-build-images/lambda:nodejs14.x
43+
container: public.ecr.aws/sam/build-nodejs14.x:latest
4244

4345
services:
4446
s3:
@@ -48,6 +50,9 @@ jobs:
4850
MINIO_SECRET_KEY: testtest
4951

5052
steps:
53+
- name: Install yarn
54+
run: npm install --global [email protected]
55+
5156
- uses: actions/checkout@v2
5257

5358
- name: Cache

lib/handler.ts

+5-11
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,14 @@ export async function handler(
9797
resMock._write = (chunk: Buffer | string) => {
9898
resMock.write(chunk);
9999
};
100-
const mockHeaders: Record<string, string | string[]> = {};
100+
const mockHeaders: Map<string, string | string[]> = new Map();
101101
resMock.writeHead = (_status: any, _headers: any) =>
102102
Object.assign(mockHeaders, _headers);
103-
resMock.getHeader = (name: string) => mockHeaders[name.toLowerCase()];
103+
resMock.getHeader = (name: string) => mockHeaders.get(name.toLowerCase());
104104
resMock.getHeaders = () => mockHeaders;
105105
resMock.getHeaderNames = () => Object.keys(mockHeaders);
106106
resMock.setHeader = (name: string, value: string | string[]) =>
107-
(mockHeaders[name.toLowerCase()] = value);
107+
mockHeaders.set(name.toLowerCase(), value);
108108
// Empty function is tolerable here since it is part of a mock
109109
// eslint-disable-next-line @typescript-eslint/no-empty-function
110110
resMock._implicitHeader = () => {};
@@ -117,7 +117,7 @@ export async function handler(
117117
};
118118

119119
const parsedUrl = parseUrl(reqMock.url, true);
120-
const result = await imageOptimizer(
120+
await imageOptimizer(
121121
imageConfig,
122122
reqMock as IncomingMessage,
123123
resMock,
@@ -126,7 +126,7 @@ export async function handler(
126126
);
127127

128128
const normalizedHeaders: Record<string, string> = {};
129-
for (const [headerKey, headerValue] of Object.entries(mockHeaders)) {
129+
for (const [headerKey, headerValue] of mockHeaders.entries()) {
130130
if (Array.isArray(headerValue)) {
131131
normalizedHeaders[headerKey] = headerValue.join(', ');
132132
continue;
@@ -135,12 +135,6 @@ export async function handler(
135135
normalizedHeaders[headerKey] = headerValue;
136136
}
137137

138-
if (result.originCacheControl) {
139-
normalizedHeaders['cache-control'] = result.originCacheControl;
140-
} else {
141-
normalizedHeaders['cache-control'] = 'public, max-age=60';
142-
}
143-
144138
if (didCallEnd) defer.resolve();
145139
await defer.promise;
146140

lib/image-optimizer.ts

+41-66
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,23 @@
11
import { IncomingMessage, ServerResponse } from 'http';
2+
import { UrlWithParsedQuery } from 'url';
3+
4+
import {
5+
imageOptimizer as pixel,
6+
ImageOptimizerOptions,
7+
} from '@millihq/pixel-core';
28
import { ImageConfig } from 'next/dist/server/image-config';
3-
import { NextConfig } from 'next/dist/server/config';
4-
import { imageOptimizer as nextImageOptimizer } from 'next/dist/server/image-optimizer';
5-
import Server from 'next/dist/server/next-server';
69
import nodeFetch from 'node-fetch';
7-
import { UrlWithParsedQuery } from 'url';
810
import S3 from 'aws-sdk/clients/s3';
911

1012
/* -----------------------------------------------------------------------------
1113
* Types
1214
* ---------------------------------------------------------------------------*/
1315

14-
type NodeFetch = typeof nodeFetch;
15-
16-
type OriginCacheControl = string | null;
17-
1816
interface S3Config {
1917
s3: S3;
2018
bucket: string;
2119
}
2220

23-
type ImageOptimizerResult = {
24-
finished: boolean;
25-
originCacheControl: OriginCacheControl;
26-
};
27-
28-
/* -----------------------------------------------------------------------------
29-
* globals
30-
* ---------------------------------------------------------------------------*/
31-
32-
// Sets working dir of Next.js to /tmp (Lambda tmp dir)
33-
const distDir = '/tmp';
34-
35-
let originCacheControl: OriginCacheControl;
36-
37-
/**
38-
* fetch polyfill to intercept the request to the external resource
39-
* to get the Cache-Control header from the origin
40-
*/
41-
const fetchPolyfill: NodeFetch = (url, init) => {
42-
return nodeFetch(url, init).then((result) => {
43-
originCacheControl = result.headers.get('Cache-Control');
44-
return result;
45-
});
46-
};
47-
48-
fetchPolyfill.isRedirect = nodeFetch.isRedirect;
49-
50-
// Polyfill fetch is used by nextImageOptimizer
51-
// @ts-ignore
52-
global.fetch = fetchPolyfill;
53-
5421
/* -----------------------------------------------------------------------------
5522
* imageOptimizer
5623
* ---------------------------------------------------------------------------*/
@@ -61,19 +28,31 @@ async function imageOptimizer(
6128
res: ServerResponse,
6229
parsedUrl: UrlWithParsedQuery,
6330
s3Config?: S3Config
64-
): Promise<ImageOptimizerResult> {
65-
// Create next config mock
66-
const nextConfig = ({
67-
images: imageConfig,
68-
} as unknown) as NextConfig;
69-
70-
// Create Next Server mock
71-
const server = {
72-
getRequestHandler: () => async (
31+
): ReturnType<typeof pixel> {
32+
const options: ImageOptimizerOptions = {
33+
/**
34+
* Use default temporary folder from AWS Lambda
35+
*/
36+
distDir: '/tmp',
37+
38+
imageConfig: {
39+
...imageConfig,
40+
loader: 'default',
41+
},
42+
43+
/**
44+
* Is called when the path is an absolute URI, e.g. `/my/image.png`.
45+
*
46+
* @param req - Incoming client request
47+
* @param res - Outgoing mocked response
48+
* @param url - Parsed url object from the client request,
49+
* e.g. `/my/image.png`
50+
*/
51+
async requestHandler(
7352
{ headers }: IncomingMessage,
7453
res: ServerResponse,
7554
url: UrlWithParsedQuery
76-
) => {
55+
) {
7756
if (s3Config) {
7857
// S3 expects keys without leading `/`
7958
const trimmedKey = url.href.startsWith('/')
@@ -98,10 +77,12 @@ async function imageOptimizer(
9877
}
9978

10079
if (object.CacheControl) {
101-
originCacheControl = object.CacheControl;
80+
res.setHeader('Cache-Control', object.CacheControl);
81+
// originCacheControl = object.CacheControl;
10282
}
10383

104-
res.end(object.Body);
84+
res.write(object.Body);
85+
res.end();
10586
} else if (headers.referer) {
10687
const { referer } = headers;
10788
const trimmedReferer = referer.endsWith('/')
@@ -116,30 +97,24 @@ async function imageOptimizer(
11697

11798
res.statusCode = upstreamRes.status;
11899
const upstreamType = upstreamRes.headers.get('Content-Type');
119-
originCacheControl = upstreamRes.headers.get('Cache-Control');
100+
const originCacheControl = upstreamRes.headers.get('Cache-Control');
120101

121102
if (upstreamType) {
122103
res.setHeader('Content-Type', upstreamType);
123104
}
124105

106+
if (originCacheControl) {
107+
res.setHeader('Cache-Control', originCacheControl);
108+
}
109+
125110
const upstreamBuffer = Buffer.from(await upstreamRes.arrayBuffer());
126-
res.end(upstreamBuffer);
111+
res.write(upstreamBuffer);
112+
res.end();
127113
}
128114
},
129-
} as Server;
130-
131-
const result = await nextImageOptimizer(
132-
server,
133-
req,
134-
res,
135-
parsedUrl,
136-
nextConfig,
137-
distDir
138-
);
139-
return {
140-
...result,
141-
originCacheControl,
142115
};
116+
117+
return pixel(req, res, parsedUrl, options);
143118
}
144119

145120
export type { S3Config };

lib/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"postpack": "rm ./LICENSE ./third-party-licenses.txt"
1717
},
1818
"dependencies": {
19+
"@millihq/pixel-core": "^1.0.0-canary.1",
1920
"aws-sdk": "*",
2021
"next": "12.0.0",
2122
"node-fetch": "2.6.1",

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
},
1919
"devDependencies": {
2020
"@dealmore/sammy": "^1.5.0",
21+
"@tsconfig/node14": "^1.0.1",
2122
"@types/jest": "^27.0.1",
2223
"@types/mime": "^2.0.3",
2324
"@types/node": "^14.0.0",

test/e2e.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('[e2e]', () => {
1616
const s3Endpoint = `${hostIpAddress}:9000`;
1717
const pathToWorker = path.resolve(__dirname, '../lib');
1818
const fixturesDir = path.resolve(__dirname, './fixtures');
19-
const cacheControlHeader = 'public, max-age=123456';
19+
const cacheControlHeader = 'public, max-age=123456, must-revalidate';
2020
let fixtureBucketName: string;
2121
let s3: S3;
2222

test/utils/run-optimizer.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { fork } from 'child_process';
2+
import { EventEmitter } from 'events';
3+
import { URLSearchParams } from 'url';
4+
25
import getPort from 'get-port';
36
import { ImageConfig } from 'next/dist/server/image-config';
47
import fetch from 'node-fetch';
58
import { createRequest, createResponse } from 'node-mocks-http';
6-
import { EventEmitter } from 'events';
79
import S3 from 'aws-sdk/clients/s3';
810

911
import { imageOptimizer } from '../../lib/image-optimizer';

tsconfig.json

+1-13
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,3 @@
11
{
2-
"compilerOptions": {
3-
"target": "es2019",
4-
"lib": ["es2019", "DOM"],
5-
"outDir": "dist",
6-
"module": "commonjs",
7-
"moduleResolution": "node",
8-
"strict": true,
9-
"declaration": false,
10-
"sourceMap": false,
11-
"experimentalDecorators": true,
12-
"esModuleInterop": true
13-
},
14-
"exclude": ["node_modules"]
2+
"extends": "@tsconfig/node14/tsconfig.json"
153
}

yarn.lock

+37
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,13 @@
796796
semver "^7.3.4"
797797
tar "^6.1.0"
798798

799+
"@millihq/pixel-core@^1.0.0-canary.1":
800+
version "1.0.0-canary.1"
801+
resolved "https://registry.yarnpkg.com/@millihq/pixel-core/-/pixel-core-1.0.0-canary.1.tgz#ce4affa4a42b819a9ef7bf426b519db3bce6b2af"
802+
integrity sha512-aBeMxUyDew5eeyam2Rfi7NAcBws/uygwzQ6UVL55ZvITkl+13uzajkPo0APVFETscFrHoYE9AWpdQkOrJ2HdQw==
803+
dependencies:
804+
node-fetch "^2.0.0"
805+
799806
"@napi-rs/triples@^1.0.3":
800807
version "1.0.3"
801808
resolved "https://registry.yarnpkg.com/@napi-rs/triples/-/triples-1.0.3.tgz#76d6d0c3f4d16013c61e45dfca5ff1e6c31ae53c"
@@ -935,6 +942,11 @@
935942
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
936943
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
937944

945+
"@tsconfig/node14@^1.0.1":
946+
version "1.0.1"
947+
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2"
948+
integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==
949+
938950
939951
version "8.10.56"
940952
resolved "https://registry.yarnpkg.com/@types/aws-lambda/-/aws-lambda-8.10.56.tgz#24fc61bf628db86412bb4f28da051df4baa532d6"
@@ -4519,6 +4531,13 @@ [email protected], node-fetch@^2.6.1:
45194531
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
45204532
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
45214533

4534+
node-fetch@^2.0.0:
4535+
version "2.6.6"
4536+
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
4537+
integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==
4538+
dependencies:
4539+
whatwg-url "^5.0.0"
4540+
45224541
node-gyp-build@^4.2.2:
45234542
version "4.2.3"
45244543
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739"
@@ -5951,6 +5970,11 @@ tr46@^2.1.0:
59515970
dependencies:
59525971
punycode "^2.1.1"
59535972

5973+
tr46@~0.0.3:
5974+
version "0.0.3"
5975+
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
5976+
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
5977+
59545978
"traverse@>=0.3.0 <0.4":
59555979
version "0.3.9"
59565980
resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9"
@@ -6225,6 +6249,11 @@ [email protected]:
62256249
glob-to-regexp "^0.4.1"
62266250
graceful-fs "^4.1.2"
62276251

6252+
webidl-conversions@^3.0.0:
6253+
version "3.0.1"
6254+
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
6255+
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
6256+
62286257
webidl-conversions@^4.0.2:
62296258
version "4.0.2"
62306259
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
@@ -6252,6 +6281,14 @@ whatwg-mimetype@^2.3.0:
62526281
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
62536282
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
62546283

6284+
whatwg-url@^5.0.0:
6285+
version "5.0.0"
6286+
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
6287+
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
6288+
dependencies:
6289+
tr46 "~0.0.3"
6290+
webidl-conversions "^3.0.0"
6291+
62556292
whatwg-url@^7.0.0:
62566293
version "7.1.0"
62576294
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"

0 commit comments

Comments
 (0)