Skip to content

Commit

Permalink
Use nft for bundling (#24)
Browse files Browse the repository at this point in the history
* Use nft for bundling

* Generate third-party license file on publish
  • Loading branch information
ofhouse authored Mar 30, 2021
1 parent 7c446bd commit cf02af8
Show file tree
Hide file tree
Showing 12 changed files with 409 additions and 101 deletions.
5 changes: 4 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
*

!package.json
!yarn.lock
!tsconfig.json
!lib/package.json
!scripts/*
!lib/*.json
!lib/**/*.ts
11 changes: 6 additions & 5 deletions buildimage.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

FROM amazon/aws-sam-cli-emulation-image-nodejs14.x

# Install yarn
RUN npm i -g yarn

WORKDIR /app

ADD tsconfig.json \
lib \
/app/
COPY . .

RUN npm i &&\
npm run build
RUN yarn --frozen-lockfile &&\
yarn build
4 changes: 4 additions & 0 deletions lib/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
dist
dist.zip

# Temporary license files
third-party-licenses.txt
LICENSE
16 changes: 11 additions & 5 deletions lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
"url": "https://github.com/dealmore/terraform-aws-next-js-image-optimization.git"
},
"scripts": {
"build": "ncc-zip build -e aws-sdk -f handler --license third-party-licenses.txt -m handler.ts",
"prepack": "cp ../LICENSE dist/third-party-licenses.txt ./",
"build:tsc": "tsc --skipLibCheck",
"build:bundle": "node ../scripts/bundle.js",
"build:licenses": "yarn licenses generate-disclaimer > third-party-licenses.txt",
"build": "yarn build:tsc && yarn build:bundle",
"prepack": "cp ../LICENSE && yarn build:licenses",
"postpack": "rm ./LICENSE ./third-party-licenses.txt"
},
"dependencies": {
Expand All @@ -18,10 +21,13 @@
},
"devDependencies": {
"@types/aws-lambda": "8.10.56",
"@types/node": "^12.0.0",
"@types/node": "^14.0.0",
"@types/node-fetch": "^2.5.7",
"@vercel/ncc": "^0.27.0",
"ncc-zip": "^1.0.0",
"@types/react": "^17.0.3",
"@types/react-dom": "^17.0.3",
"@vercel/nft": "^0.9.6",
"archiver": "^5.3.0",
"glob": "^7.1.6",
"typescript": "^4.1.3"
},
"files": [
Expand Down
3 changes: 3 additions & 0 deletions lib/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "./dist"
},
"include": ["./**/*"]
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
"lib"
],
"scripts": {
"build": "yarn workspace @dealmore/tf-next-image-optimization build",
"build:local": "scripts/build-local.sh",
"test": "jest --testPathIgnorePatterns e2e.*",
"test:e2e": "jest --testTimeout=60000 e2e.*"
"test:e2e": "jest e2e.*"
},
"devDependencies": {
"@dealmore/sammy": "^1.5.0",
Expand Down
2 changes: 1 addition & 1 deletion scripts/build-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ docker build -t "${IMAGE_NAME}" -f buildimage.Dockerfile .
# Copy the artifact back to the host machine
# https://stackoverflow.com/a/31316636/831465
id=$(docker create local/tf-next-image-optiomizer-build sh)
docker cp $id:/app/dist.zip ./lib/dist.zip
docker cp $id:/app/lib/dist.zip ./lib/dist.zip
docker rm -v $id
54 changes: 54 additions & 0 deletions scripts/bundle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Creates a bundled zip file with all dependencies for AWS Lambda
*/

const path = require('path');
const fs = require('fs');
const { nodeFileTrace } = require('@vercel/nft');
const glob = require('glob');
const archiver = require('archiver');

const workspaceRoot = path.resolve(__dirname, '..');
const buildDir = path.resolve(workspaceRoot, 'lib/dist');

async function main() {
// Get all files from build dir
const buildFiles = glob.sync('**/*.js', { cwd: buildDir, absolute: true });

console.log('buildFiles', buildFiles);

const { fileList } = await nodeFileTrace(buildFiles, {
base: workspaceRoot,
processCwd: process.cwd(),
// aws-sdk is already provided in Lambda images
ignore: ['**/aws-sdk/**/*'],
});

// Create zip file
await new Promise((resolve, reject) => {
const outputFile = fs.createWriteStream(
path.resolve(workspaceRoot, 'lib/dist.zip')
);

outputFile.on('close', () => resolve());
outputFile.on('error', (error) => reject(error));

const archive = archiver('zip', {
zlib: { level: 5 }, // Optimal compression
});
archive.pipe(outputFile);

for (const file of fileList) {
// Remove lib/ and lib/dist/ prefix
const fileName = file.replace(/^lib\/(dist\/)?/, '');

archive.append(fs.createReadStream(path.join(workspaceRoot, file)), {
name: fileName,
mode: 0o666,
});
}
archive.finalize();
});
}

main();
125 changes: 70 additions & 55 deletions test/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { s3PublicDir } from './utils/s3-public-dir';
import { getLocalIpAddressFromHost } from './utils/host-ip-address';
import { acceptAllFixtures } from './constants';

const ONE_MINUTE_IN_MS = 60000;
const NODE_RUNTIME = 'nodejs14.x';

describe('[e2e]', () => {
Expand Down Expand Up @@ -107,7 +108,8 @@ describe('[e2e]', () => {
// Header settings needed for CloudFront compression
expect(response.headers.has('Content-Length')).toBeTruthy();
expect(response.headers.has('Content-Encoding')).toBeFalsy();
}
},
ONE_MINUTE_IN_MS
);

test.each(acceptAllFixtures)(
Expand Down Expand Up @@ -154,7 +156,8 @@ describe('[e2e]', () => {
// Header settings needed for CloudFront compression
expect(response.headers.has('Content-Length')).toBeTruthy();
expect(response.headers.has('Content-Encoding')).toBeFalsy();
}
},
ONE_MINUTE_IN_MS
);
});

Expand Down Expand Up @@ -201,28 +204,32 @@ describe('[e2e]', () => {
await lambdaSAM.stop();
});

test('Internal: Fetch image from S3', async () => {
const fixture = acceptAllFixtures[0];
const publicPath = `/${fixture[0]}`;
const optimizerParams = new URLSearchParams({
url: publicPath,
w: '2048',
q: '75',
});
test(
'Internal: Fetch image from S3',
async () => {
const fixture = acceptAllFixtures[0];
const publicPath = `/${fixture[0]}`;
const optimizerParams = new URLSearchParams({
url: publicPath,
w: '2048',
q: '75',
});

const response = await lambdaSAM.sendApiGwRequest(
`${route}?${optimizerParams.toString()}`
);
const response = await lambdaSAM.sendApiGwRequest(
`${route}?${optimizerParams.toString()}`
);

const body = await response
.text()
.then((text) => Buffer.from(text, 'base64'));
const body = await response
.text()
.then((text) => Buffer.from(text, 'base64'));

expect(response.ok).toBeTruthy();
expect(response.headers.get('content-type')).toBe(
fixture[1]['content-type']
);
});
expect(response.ok).toBeTruthy();
expect(response.headers.get('content-type')).toBe(
fixture[1]['content-type']
);
},
ONE_MINUTE_IN_MS
);
});

describe('From filesystem cache for external image', () => {
Expand Down Expand Up @@ -256,40 +263,48 @@ describe('[e2e]', () => {
await lambdaSAM.stop();
});
test.each([
"internet. (first call)",
"hitting filesystem cache. (2nd call)",
])("Fetch external image by %s", async () => {
const [filePath, fixtureResponse] = acceptAllFixtures.find(f => f[1].ext === 'png') || []
if (!filePath || !fixtureResponse) throw new Error('Can not found png file path')

const publicPath = `http://${s3Endpoint}/${fixtureBucketName}/${filePath}`;
const [w,q] = ['2048','75']
const optimizerParams = new URLSearchParams({ url: publicPath, w, q })
const optimizerPrefix = `external_accept_all_w-${w}_q-${q}_`;
const snapshotFileName = path.join(
__dirname,
'__snapshots__/e2e/',
`${optimizerPrefix}${filePath.replace('/', '_')}.${fixtureResponse.ext}`
);

const response = await lambdaSAM.sendApiGwRequest(
`${route}?${optimizerParams.toString()}`
);
const text = await response.text();
const body = Buffer.from(text, 'base64');

expect(response.status).toBe(200);
expect(body).toMatchFile(snapshotFileName);
expect(response.headers.get('Content-Type')).toBe(
fixtureResponse['content-type']
);
expect(response.headers.get('Cache-Control')).toBe(cacheControlHeader);

// Header settings needed for CloudFront compression
expect(response.headers.has('Content-Length')).toBeTruthy();
expect(response.headers.has('Content-Encoding')).toBeFalsy();
})
})
'internet. (first call)',
'hitting filesystem cache. (2nd call)',
])(
'Fetch external image by %s',
async () => {
const [filePath, fixtureResponse] =
acceptAllFixtures.find((f) => f[1].ext === 'png') || [];
if (!filePath || !fixtureResponse)
throw new Error('Can not found png file path');

const publicPath = `http://${s3Endpoint}/${fixtureBucketName}/${filePath}`;
const [w, q] = ['2048', '75'];
const optimizerParams = new URLSearchParams({ url: publicPath, w, q });
const optimizerPrefix = `external_accept_all_w-${w}_q-${q}_`;
const snapshotFileName = path.join(
__dirname,
'__snapshots__/e2e/',
`${optimizerPrefix}${filePath.replace('/', '_')}.${
fixtureResponse.ext
}`
);

const response = await lambdaSAM.sendApiGwRequest(
`${route}?${optimizerParams.toString()}`
);
const text = await response.text();
const body = Buffer.from(text, 'base64');

expect(response.status).toBe(200);
expect(body).toMatchFile(snapshotFileName);
expect(response.headers.get('Content-Type')).toBe(
fixtureResponse['content-type']
);
expect(response.headers.get('Cache-Control')).toBe(cacheControlHeader);

// Header settings needed for CloudFront compression
expect(response.headers.has('Content-Length')).toBeTruthy();
expect(response.headers.has('Content-Encoding')).toBeFalsy();
},
ONE_MINUTE_IN_MS
);
});

test.todo('Run test against domain that is not on the list');
});
2 changes: 1 addition & 1 deletion test/utils/host-ip-address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export function getLocalIpAddressFromHost() {
const results: Record<string, Array<string>> = {}; // or just '{}', an empty object

for (const name of Object.keys(nets)) {
for (const net of nets[name]) {
for (const net of nets[name]!) {
// skip over non-ipv4 and internal (i.e. 127.0.0.1) addresses
if (net.family === 'IPv4' && !net.internal) {
if (!results[name]) {
Expand Down
5 changes: 3 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"target": "es2019",
"lib": ["es2019"],
"lib": ["es2019", "DOM"],
"outDir": "dist",
"module": "commonjs",
"moduleResolution": "node",
Expand All @@ -10,5 +10,6 @@
"sourceMap": false,
"experimentalDecorators": true,
"esModuleInterop": true
}
},
"exclude": ["node_modules"]
}
Loading

0 comments on commit cf02af8

Please sign in to comment.