Skip to content

Commit 6c551c3

Browse files
authored
Adds eslint (#65)
1 parent 7820a71 commit 6c551c3

15 files changed

+1087
-75
lines changed

.eslintignore

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
**/node_modules
2+
3+
# Build output
4+
/lib/dist

.eslintrc.js

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
'use strict';
2+
3+
const OFF = 0;
4+
5+
module.exports = {
6+
root: true,
7+
parser: '@typescript-eslint/parser',
8+
plugins: ['@typescript-eslint', 'jest'],
9+
env: {
10+
node: true,
11+
es6: true,
12+
'jest/globals': true,
13+
},
14+
extends: [
15+
'eslint:recommended',
16+
'plugin:import/recommended',
17+
'plugin:jest/recommended',
18+
],
19+
20+
overrides: [
21+
{
22+
files: ['**/*.ts', '**/*.tsx'],
23+
extends: [
24+
'plugin:@typescript-eslint/eslint-recommended',
25+
'plugin:@typescript-eslint/recommended',
26+
'plugin:import/typescript',
27+
],
28+
rules: {
29+
'@typescript-eslint/no-explicit-any': OFF,
30+
'@typescript-eslint/ban-ts-comment': OFF,
31+
},
32+
},
33+
],
34+
};

.github/workflows/lint.yml

+4-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88

99
jobs:
1010
lint-js:
11-
name: Lint JavaScript
11+
name: Lint Type- & JavaScript
1212
runs-on: ubuntu-latest
1313
steps:
1414
- name: Checkout
@@ -28,6 +28,9 @@ jobs:
2828
- name: Lint Prettier
2929
run: yarn lint:prettier
3030

31+
- name: Lint ESLint
32+
run: yarn lint:eslint
33+
3134
lint-tf:
3235
name: Lint Terraform
3336
runs-on: ubuntu-latest

lib/declarations.d.ts

+17-11
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
1-
declare module NodeJS {
2-
interface Global {
3-
fetch: any;
4-
}
1+
import nodeFetch from 'node-fetch';
52

6-
export interface ProcessEnv {
7-
TF_NEXTIMAGE_DOMAINS?: string;
8-
TF_NEXTIMAGE_DEVICE_SIZES?: string;
9-
TF_NEXTIMAGE_IMAGE_SIZES?: string;
10-
TF_NEXTIMAGE_SOURCE_BUCKET?: string;
11-
__DEBUG__USE_LOCAL_BUCKET?: string;
12-
NEXT_SHARP_PATH?: string;
3+
type NodeFetch = typeof nodeFetch;
4+
declare global {
5+
namespace NodeJS {
6+
interface Global {
7+
fetch: NodeFetch;
8+
}
9+
export interface ProcessEnv {
10+
TF_NEXTIMAGE_DOMAINS?: string;
11+
TF_NEXTIMAGE_DEVICE_SIZES?: string;
12+
TF_NEXTIMAGE_IMAGE_SIZES?: string;
13+
TF_NEXTIMAGE_SOURCE_BUCKET?: string;
14+
__DEBUG__USE_LOCAL_BUCKET?: string;
15+
NEXT_SHARP_PATH?: string;
16+
}
1317
}
1418
}
19+
20+
export {};

lib/handler.ts

+14-7
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ process.env.NEXT_SHARP_PATH = require.resolve('sharp');
44

55
import { ImageConfig, imageConfigDefault } from 'next/dist/server/image-config';
66
import { parse as parseUrl } from 'url';
7-
import {
7+
import type {
88
APIGatewayProxyEventV2,
99
APIGatewayProxyStructuredResultV2,
10+
// Disable is tolerable since we only import the types here, not the module
11+
// itself
12+
// eslint-disable-next-line import/no-unresolved
1013
} from 'aws-lambda';
1114
import { Writable } from 'stream';
1215
import S3 from 'aws-sdk/clients/s3';
16+
import { IncomingMessage } from 'http';
1317

1418
import { imageOptimizer, S3Config } from './image-optimizer';
1519
import { normalizeHeaders } from './normalized-headers';
@@ -37,8 +41,9 @@ function generateS3Config(bucketName?: string): S3Config | undefined {
3741

3842
function parseFromEnv<T>(key: string, defaultValue: T) {
3943
try {
40-
if (key in process.env) {
41-
return JSON.parse(process.env[key]!) as T;
44+
const envValue = process.env[key];
45+
if (typeof envValue === 'string') {
46+
return JSON.parse(envValue) as T;
4247
}
4348

4449
return defaultValue;
@@ -51,7 +56,7 @@ function parseFromEnv<T>(key: string, defaultValue: T) {
5156

5257
const domains = parseFromEnv(
5358
'TF_NEXTIMAGE_DOMAINS',
54-
imageConfigDefault.domains!
59+
imageConfigDefault.domains ?? []
5560
);
5661
const deviceSizes = parseFromEnv(
5762
'TF_NEXTIMAGE_DEVICE_SIZES',
@@ -75,7 +80,7 @@ export async function handler(
7580
): Promise<APIGatewayProxyStructuredResultV2> {
7681
const s3Config = generateS3Config(sourceBucket);
7782

78-
const reqMock: any = {
83+
const reqMock = {
7984
headers: normalizeHeaders(event.headers),
8085
method: event.requestContext.http.method,
8186
url: `/?${event.rawQueryString}`,
@@ -100,19 +105,21 @@ export async function handler(
100105
resMock.getHeaderNames = () => Object.keys(mockHeaders);
101106
resMock.setHeader = (name: string, value: string | string[]) =>
102107
(mockHeaders[name.toLowerCase()] = value);
108+
// Empty function is tolerable here since it is part of a mock
109+
// eslint-disable-next-line @typescript-eslint/no-empty-function
103110
resMock._implicitHeader = () => {};
104111

105112
resMock.originalEnd = resMock.end;
106113
resMock.on('close', () => defer.resolve());
107-
resMock.end = (message: any) => {
114+
resMock.end = (message: string) => {
108115
didCallEnd = true;
109116
resMock.originalEnd(message);
110117
};
111118

112119
const parsedUrl = parseUrl(reqMock.url, true);
113120
const result = await imageOptimizer(
114121
imageConfig,
115-
reqMock,
122+
reqMock as IncomingMessage,
116123
resMock,
117124
parsedUrl,
118125
s3Config

lib/image-optimizer.ts

+38-16
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,65 @@ import { ImageConfig } from 'next/dist/server/image-config';
33
import { NextConfig } from 'next/dist/server/config';
44
import { imageOptimizer as nextImageOptimizer } from 'next/dist/server/image-optimizer';
55
import Server from 'next/dist/server/next-server';
6-
import nodeFetch, { RequestInfo, RequestInit } from 'node-fetch';
6+
import nodeFetch from 'node-fetch';
77
import { UrlWithParsedQuery } from 'url';
88
import S3 from 'aws-sdk/clients/s3';
99

10+
/* -----------------------------------------------------------------------------
11+
* Types
12+
* ---------------------------------------------------------------------------*/
13+
14+
type NodeFetch = typeof nodeFetch;
15+
16+
type OriginCacheControl = string | null;
17+
18+
interface S3Config {
19+
s3: S3;
20+
bucket: string;
21+
}
22+
23+
type ImageOptimizerResult = {
24+
finished: boolean;
25+
originCacheControl: OriginCacheControl;
26+
};
27+
28+
/* -----------------------------------------------------------------------------
29+
* globals
30+
* ---------------------------------------------------------------------------*/
31+
1032
// Sets working dir of Next.js to /tmp (Lambda tmp dir)
1133
const distDir = '/tmp';
1234

13-
let originCacheControl: string | null;
35+
let originCacheControl: OriginCacheControl;
1436

1537
/**
1638
* fetch polyfill to intercept the request to the external resource
1739
* to get the Cache-Control header from the origin
1840
*/
19-
function fetchPolyfill(url: RequestInfo, init?: RequestInit) {
41+
const fetchPolyfill: NodeFetch = (url, init) => {
2042
return nodeFetch(url, init).then((result) => {
2143
originCacheControl = result.headers.get('Cache-Control');
2244
return result;
2345
});
24-
}
46+
};
2547

26-
// Polyfill fetch used by nextImageOptimizer
48+
fetchPolyfill.isRedirect = nodeFetch.isRedirect;
49+
50+
// Polyfill fetch is used by nextImageOptimizer
51+
// @ts-ignore
2752
global.fetch = fetchPolyfill;
2853

29-
interface S3Config {
30-
s3: S3;
31-
bucket: string;
32-
}
54+
/* -----------------------------------------------------------------------------
55+
* imageOptimizer
56+
* ---------------------------------------------------------------------------*/
3357

3458
async function imageOptimizer(
3559
imageConfig: ImageConfig,
3660
req: IncomingMessage,
3761
res: ServerResponse,
3862
parsedUrl: UrlWithParsedQuery,
3963
s3Config?: S3Config
40-
) {
64+
): Promise<ImageOptimizerResult> {
4165
// Create next config mock
4266
const nextConfig = ({
4367
images: imageConfig,
@@ -79,9 +103,6 @@ async function imageOptimizer(
79103

80104
res.end(object.Body);
81105
} else if (headers.referer) {
82-
let upstreamBuffer: Buffer;
83-
let upstreamType: string | null;
84-
85106
const { referer } = headers;
86107
const trimmedReferer = referer.endsWith('/')
87108
? referer.substring(0, referer.length - 1)
@@ -94,14 +115,14 @@ async function imageOptimizer(
94115
}
95116

96117
res.statusCode = upstreamRes.status;
97-
upstreamBuffer = Buffer.from(await upstreamRes.arrayBuffer());
98-
upstreamType = upstreamRes.headers.get('Content-Type');
118+
const upstreamType = upstreamRes.headers.get('Content-Type');
99119
originCacheControl = upstreamRes.headers.get('Cache-Control');
100120

101121
if (upstreamType) {
102122
res.setHeader('Content-Type', upstreamType);
103123
}
104124

125+
const upstreamBuffer = Buffer.from(await upstreamRes.arrayBuffer());
105126
res.end(upstreamBuffer);
106127
}
107128
},
@@ -121,4 +142,5 @@ async function imageOptimizer(
121142
};
122143
}
123144

124-
export { S3Config, imageOptimizer };
145+
export type { S3Config };
146+
export { imageOptimizer };

lib/normalized-headers.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { IncomingHttpHeaders } from 'http';
33
/**
44
* Normalizes the headers from API Gateway 2.0 format
55
*/
6-
export function normalizeHeaders(headers: Record<string, string>) {
6+
export function normalizeHeaders(
7+
headers: Record<string, string>
8+
): IncomingHttpHeaders {
79
const _headers: IncomingHttpHeaders = {};
810

911
for (const [key, value] of Object.entries(headers)) {

lib/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export interface Deferred<T = never> {
44
reject: (reason?: any) => void;
55
}
66

7-
export function createDeferred<T>() {
7+
export function createDeferred<T>(): Deferred<T> {
88
let r;
99
let j;
1010

package.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
"build:local": "scripts/build-local.sh",
1212
"test": "yarn build && jest --testPathIgnorePatterns e2e.*",
1313
"test:e2e": "jest e2e.*",
14+
"lint:eslint": "eslint .",
15+
"fix:eslint": "eslint . --fix",
1416
"lint:prettier": "prettier --check .",
1517
"fix:prettier": "prettier --write ."
1618
},
@@ -20,7 +22,12 @@
2022
"@types/mime": "^2.0.3",
2123
"@types/node": "^14.0.0",
2224
"@types/node-fetch": "^2.5.10",
25+
"@typescript-eslint/eslint-plugin": "^4.30.0",
26+
"@typescript-eslint/parser": "^4.30.0",
2327
"aws-sdk": "*",
28+
"eslint": "^7.32.0",
29+
"eslint-plugin-import": "^2.24.2",
30+
"eslint-plugin-jest": "^24.4.0",
2431
"get-port": "^5.1.1",
2532
"jest": "^27.0.6",
2633
"jest-file-snapshot": "^0.5.0",
@@ -29,7 +36,7 @@
2936
"node-mocks-http": "^1.10.0",
3037
"prettier": "^2.2.1",
3138
"ts-jest": "^27.0.5",
32-
"typescript": "^4.1.3"
39+
"typescript": "^4.4.2"
3340
},
3441
"resolutions": {
3542
"aws-sdk": "2.880.0"

test/utils/generate-params.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
import { parse as parseUrl, URLSearchParams } from 'url';
1+
import { parse as parseUrl, URLSearchParams, UrlWithParsedQuery } from 'url';
22

3-
interface Options {
3+
type GenerateParamsOptions = {
44
w?: string;
55
q?: string;
6-
}
6+
};
7+
8+
type GenerateParamsResult = {
9+
url: string;
10+
parsedUrl: UrlWithParsedQuery;
11+
params: Record<string, string>;
12+
};
713

8-
export function generateParams(url: string, options: Options = {}) {
14+
export function generateParams(
15+
url: string,
16+
options: GenerateParamsOptions = {}
17+
): GenerateParamsResult {
918
const encodedUrl = encodeURIComponent(url);
1019
const params = new URLSearchParams();
1120
params.append('url', url);

test/utils/host-ip-address.ts

+12-8
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@ import { networkInterfaces } from 'os';
44
* Utility to find the local ip address
55
* @see: https://stackoverflow.com/a/8440736/831465
66
*/
7-
export function getLocalIpAddressFromHost() {
7+
export function getLocalIpAddressFromHost(): string | undefined {
88
const nets = networkInterfaces();
99
const results: Record<string, Array<string>> = {}; // or just '{}', an empty object
1010

1111
for (const name of Object.keys(nets)) {
12-
for (const net of nets[name]!) {
13-
// skip over non-ipv4 and internal (i.e. 127.0.0.1) addresses
14-
if (net.family === 'IPv4' && !net.internal) {
15-
if (!results[name]) {
16-
results[name] = [];
17-
}
12+
const netsByName = nets[name];
13+
14+
if (netsByName !== undefined) {
15+
for (const net of netsByName) {
16+
// skip over non-ipv4 and internal (i.e. 127.0.0.1) addresses
17+
if (net.family === 'IPv4' && !net.internal) {
18+
if (!results[name]) {
19+
results[name] = [];
20+
}
1821

19-
results[name].push(net.address);
22+
results[name].push(net.address);
23+
}
2024
}
2125
}
2226
}

0 commit comments

Comments
 (0)