@bifravst/aws-cdk-lambda-helpers 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Nordic Semiconductor ASA | nordicsemi.no
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,14 @@
1
+ # AWS CDK Lambda Helpers [![npm version](https://img.shields.io/npm/v/@bifravst/aws-cdk-lambda-helpers.svg)](https://www.npmjs.com/package/@bifravst/aws-cdk-lambda-helpers)
2
+
3
+ [![GitHub Actions](https://github.com/bifravst/aws-cdk-lambda-helpers/workflows/Test%20and%20Release/badge.svg)](https://github.com/bifravst/aws-cdk-lambda-helpers/actions)
4
+ [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
5
+ [![Renovate](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com)
6
+ [![@commitlint/config-conventional](https://img.shields.io/badge/%40commitlint-config--conventional-brightgreen)](https://github.com/conventional-changelog/commitlint/tree/master/@commitlint/config-conventional)
7
+ [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier/)
8
+ [![ESLint: TypeScript](https://img.shields.io/badge/ESLint-TypeScript-blue.svg)](https://github.com/typescript-eslint/typescript-eslint)
9
+
10
+ Helper functions which simplify working with TypeScript lambdas for AWS CDK.
11
+
12
+ ## Installation
13
+
14
+ npm i --save --save-exact @bifravst/aws-cdk-lambda-helpers
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import path from 'node:path';
4
+ import { checkSumOfFiles } from './checksumOfFiles.js';
5
+ void describe('checkSumOfFiles()', () => {
6
+ void it('should calculate a checksum of files', async () => assert.equal(await checkSumOfFiles([
7
+ // sha1sum cdk/helpers/lambdas/test-data/1.txt
8
+ // 6ae3f2029d36e029175cc225c2c4cda51a5ac602 cdk/helpers/lambdas/test-data/1.txt
9
+ path.join(process.cwd(), 'src', 'test-data', '1.txt'),
10
+ // sha1sum cdk/helpers/lambdas/test-data/2.txt
11
+ // 6a9c3333d7a3f9ee9fa1ef70224766fafb208fe4 cdk/helpers/lambdas/test-data/2.txt
12
+ path.join(process.cwd(), 'src', 'test-data', '2.txt'),
13
+ ]),
14
+ // echo -n 6ae3f2029d36e029175cc225c2c4cda51a5ac6026a9c3333d7a3f9ee9fa1ef70224766fafb208fe4 | sha1sum
15
+ 'baa003a894945a0d2519b1f4340caa97c462058f'));
16
+ });
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Computes the combined checksum of the given files
3
+ */
4
+ export declare const checkSumOfFiles: (files: string[]) => Promise<string>;
5
+ export declare const checkSumOfStrings: (strings: string[]) => string;
@@ -0,0 +1,42 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ /**
4
+ * Computes the combined checksum of the given files
5
+ */
6
+ export const checkSumOfFiles = async (files) => {
7
+ const fileChecksums = await checkSum(files);
8
+ const checksum = checkSumOfStrings([...Object.entries(fileChecksums)].map(([, hash]) => hash));
9
+ return checksum;
10
+ };
11
+ export const checkSumOfStrings = (strings) => {
12
+ const hash = crypto.createHash('sha1');
13
+ hash.update(strings.join(''));
14
+ return hash.digest('hex');
15
+ };
16
+ const hashCache = {};
17
+ const hashFile = async (file) => {
18
+ if (hashCache[file] === undefined) {
19
+ hashCache[file] = await new Promise((resolve) => {
20
+ const hash = crypto.createHash('sha1');
21
+ hash.setEncoding('hex');
22
+ const fileStream = fs.createReadStream(file);
23
+ fileStream.pipe(hash, { end: false });
24
+ fileStream.on('end', () => {
25
+ hash.end();
26
+ const h = hash.read().toString();
27
+ resolve(h);
28
+ });
29
+ });
30
+ }
31
+ return hashCache[file];
32
+ };
33
+ /**
34
+ * Computes the checksum for the given files
35
+ */
36
+ const checkSum = async (files) => {
37
+ const hashes = {};
38
+ await files.reduce(async (p, file) => p.then(async () => {
39
+ hashes[file] = await hashFile(file);
40
+ }), Promise.resolve());
41
+ return hashes;
42
+ };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Returns the common ancestor directory from a list of files
3
+ */
4
+ export declare const commonParent: (files: string[]) => string;
@@ -0,0 +1,14 @@
1
+ import { parse, sep } from 'node:path';
2
+ /**
3
+ * Returns the common ancestor directory from a list of files
4
+ */
5
+ export const commonParent = (files) => {
6
+ if (files.length === 1)
7
+ return parse(files[0] ?? '').dir + sep;
8
+ let index = 0;
9
+ let prefix = '/';
10
+ while (files.filter((f) => f.startsWith(prefix)).length === files.length) {
11
+ prefix = files[0]?.slice(0, index++) ?? '';
12
+ }
13
+ return prefix.slice(0, prefix.lastIndexOf('/') + 1);
14
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { commonParent } from './commonParent.js';
4
+ void describe('commonParent()', () => {
5
+ void it('should return the common parent directory', () => assert.equal(commonParent([
6
+ '/some/dir/lambda/onMessage.ts',
7
+ '/some/dir/lambda/notifyClients.ts',
8
+ '/some/dir/lambda/wirepasPublish.ts',
9
+ '/some/dir/wirepas-5g-mesh-gateway/protobuf/ts/data_message.ts',
10
+ ]), '/some/dir/'));
11
+ void it('should return the entire parent tree for a single file', () => assert.equal(commonParent(['/some/dir/lambda/onMessage.ts']), '/some/dir/lambda/'));
12
+ void it('should return "/" if files have no common directory', () => assert.equal(commonParent([
13
+ '/some/dir/lambda/onMessage.ts',
14
+ '/other/dir/lambda/onMessage.ts',
15
+ ]), '/'));
16
+ void it('should return the common ancestor only up until the directory level', () => assert.equal(commonParent([
17
+ '/some/dir/lambdas/cors.ts',
18
+ '/some/dir/lambdas/corsHeaders.ts',
19
+ ]), '/some/dir/lambdas/'));
20
+ });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Resolve project-level dependencies for the given file using TypeScript compiler API
3
+ */
4
+ export declare const findDependencies: (sourceFile: string, imports?: string[], visited?: string[]) => string[];
@@ -0,0 +1,42 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import ts, {} from 'typescript';
4
+ /**
5
+ * Resolve project-level dependencies for the given file using TypeScript compiler API
6
+ */
7
+ export const findDependencies = (sourceFile, imports = [], visited = []) => {
8
+ if (visited.includes(sourceFile))
9
+ return imports;
10
+ const fileNode = ts.createSourceFile(sourceFile, readFileSync(sourceFile, 'utf-8').toString(), ts.ScriptTarget.ES2022,
11
+ /*setParentNodes */ true);
12
+ const parseChild = (node) => {
13
+ if (node.kind !== ts.SyntaxKind.ImportDeclaration)
14
+ return;
15
+ const moduleSpecifier = node.moduleSpecifier.text;
16
+ const file = moduleSpecifier.startsWith('.')
17
+ ? path
18
+ .resolve(path.parse(sourceFile).dir, moduleSpecifier)
19
+ // In ECMA Script modules, all imports from local files must have an extension.
20
+ // See https://nodejs.org/api/esm.html#mandatory-file-extensions
21
+ // So we need to replace the `.js` in the import specification to find the TypeScript source for the file.
22
+ // Example: import { Network, notifyClients } from './notifyClients.js'
23
+ // The source file for that is actually in './notifyClients.ts'
24
+ .replace(/\.js$/, '.ts')
25
+ : moduleSpecifier;
26
+ try {
27
+ const s = statSync(file);
28
+ if (!s.isDirectory())
29
+ imports.push(file);
30
+ }
31
+ catch {
32
+ // Module or file not found
33
+ visited.push(file);
34
+ }
35
+ };
36
+ ts.forEachChild(fileNode, parseChild);
37
+ visited.push(sourceFile);
38
+ for (const file of imports) {
39
+ findDependencies(file, imports, visited);
40
+ }
41
+ return imports;
42
+ };
@@ -0,0 +1,18 @@
1
+ export type PackedLambda = {
2
+ id: string;
3
+ zipFile: string;
4
+ handler: string;
5
+ hash: string;
6
+ };
7
+ /**
8
+ * In the bundle we only include code that's not in the layer.
9
+ */
10
+ export declare const packLambda: ({ sourceFile, zipFile, debug, progress, }: {
11
+ sourceFile: string;
12
+ zipFile: string;
13
+ debug?: (label: string, info: string) => void;
14
+ progress?: (label: string, info: string) => void;
15
+ }) => Promise<{
16
+ handler: string;
17
+ hash: string;
18
+ }>;
@@ -0,0 +1,56 @@
1
+ import swc from '@swc/core';
2
+ import { createWriteStream } from 'node:fs';
3
+ import { parse } from 'path';
4
+ import yazl from 'yazl';
5
+ import { checkSumOfFiles } from './checksumOfFiles.js';
6
+ import { commonParent } from './commonParent.js';
7
+ import { findDependencies } from './findDependencies.js';
8
+ import { fileURLToPath } from 'node:url';
9
+ const removeCommonAncestor = (parentDir) => (file) => {
10
+ const p = parse(file);
11
+ const jsFileName = [
12
+ p.dir.replace(parentDir.slice(0, parentDir.length - 1), ''),
13
+ `${p.name}.js`,
14
+ ]
15
+ .join('/')
16
+ // Replace leading slash
17
+ .replace(/^\//, '');
18
+ return jsFileName;
19
+ };
20
+ /**
21
+ * In the bundle we only include code that's not in the layer.
22
+ */
23
+ export const packLambda = async ({ sourceFile, zipFile, debug, progress, }) => {
24
+ const lambdaFiles = [sourceFile, ...findDependencies(sourceFile)];
25
+ const zipfile = new yazl.ZipFile();
26
+ const stripCommon = removeCommonAncestor(commonParent(lambdaFiles));
27
+ for (const file of lambdaFiles) {
28
+ const compiled = (await swc.transformFile(file, {
29
+ jsc: {
30
+ target: 'es2022',
31
+ },
32
+ })).code;
33
+ debug?.(`compiled`, compiled);
34
+ const jsFileName = stripCommon(file);
35
+ zipfile.addBuffer(Buffer.from(compiled, 'utf-8'), jsFileName);
36
+ progress?.(`added`, jsFileName);
37
+ }
38
+ const hash = await checkSumOfFiles([
39
+ ...lambdaFiles,
40
+ // Include this script, so artefact is updated if the way it's built is changed
41
+ fileURLToPath(import.meta.url),
42
+ ]);
43
+ // Mark it as ES module
44
+ zipfile.addBuffer(Buffer.from(JSON.stringify({
45
+ type: 'module',
46
+ }), 'utf-8'), 'package.json');
47
+ progress?.(`added`, 'package.json');
48
+ await new Promise((resolve) => {
49
+ zipfile.outputStream.pipe(createWriteStream(zipFile)).on('close', () => {
50
+ resolve();
51
+ });
52
+ zipfile.end();
53
+ });
54
+ progress?.(`written`, zipFile);
55
+ return { handler: stripCommon(sourceFile), hash };
56
+ };
@@ -0,0 +1,2 @@
1
+ import { type PackedLambda } from './packLambda.js';
2
+ export declare const packLambdaFromPath: (id: string, sourceFile: string, handlerFunction?: string, baseDir?: string) => Promise<PackedLambda>;
@@ -0,0 +1,24 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { packLambda } from './packLambda.js';
4
+ export const packLambdaFromPath = async (id, sourceFile, handlerFunction = 'handler', baseDir = process.cwd()) => {
5
+ try {
6
+ await mkdir(path.join(process.cwd(), 'dist', 'lambdas'), {
7
+ recursive: true,
8
+ });
9
+ }
10
+ catch {
11
+ // Directory exists
12
+ }
13
+ const zipFile = path.join(process.cwd(), 'dist', 'lambdas', `${id}.zip`);
14
+ const { handler, hash } = await packLambda({
15
+ sourceFile: path.join(baseDir, sourceFile),
16
+ zipFile,
17
+ });
18
+ return {
19
+ id,
20
+ zipFile,
21
+ handler: handler.replace('.js', `.${handlerFunction}`),
22
+ hash,
23
+ };
24
+ };
@@ -0,0 +1,8 @@
1
+ export type PackedLayer = {
2
+ layerZipFile: string;
3
+ hash: string;
4
+ };
5
+ export declare const packLayer: ({ id, dependencies, }: {
6
+ id: string;
7
+ dependencies: string[];
8
+ }) => Promise<PackedLayer>;
@@ -0,0 +1,85 @@
1
+ import { spawn } from 'child_process';
2
+ import { createWriteStream } from 'fs';
3
+ import { copyFile, mkdir, readFile, rm, writeFile } from 'fs/promises';
4
+ import { glob } from 'glob';
5
+ import path from 'path';
6
+ import { ZipFile } from 'yazl';
7
+ import { checkSumOfFiles, checkSumOfStrings } from './checksumOfFiles.js';
8
+ import { fileURLToPath } from 'node:url';
9
+ export const packLayer = async ({ id, dependencies, }) => {
10
+ const packageJsonFile = path.join(process.cwd(), 'package.json');
11
+ const packageLockJsonFile = path.join(process.cwd(), 'package-lock.json');
12
+ const { dependencies: deps, devDependencies: devDeps } = JSON.parse(await readFile(packageJsonFile, 'utf-8'));
13
+ const layerDir = path.join(process.cwd(), 'dist', 'layers', id);
14
+ const nodejsDir = path.join(layerDir, 'nodejs');
15
+ try {
16
+ await rm(layerDir, { recursive: true });
17
+ }
18
+ catch {
19
+ // Folder does not exist.
20
+ }
21
+ await mkdir(nodejsDir, { recursive: true });
22
+ const depsToBeInstalled = dependencies.reduce((resolved, dep) => {
23
+ const resolvedDependency = deps[dep] ?? devDeps[dep];
24
+ if (resolvedDependency === undefined)
25
+ throw new Error(`Could not resolve dependency "${dep}" in ${packageJsonFile}!`);
26
+ return {
27
+ ...resolved,
28
+ [dep]: resolvedDependency,
29
+ };
30
+ }, {});
31
+ const packageJSON = path.join(nodejsDir, 'package.json');
32
+ await writeFile(packageJSON, JSON.stringify({
33
+ dependencies: depsToBeInstalled,
34
+ }), 'utf-8');
35
+ const packageLock = path.join(nodejsDir, 'package-lock.json');
36
+ await copyFile(packageLockJsonFile, packageLock);
37
+ await new Promise((resolve, reject) => {
38
+ const [cmd, ...args] = [
39
+ 'npm',
40
+ 'ci',
41
+ '--ignore-scripts',
42
+ '--only=prod',
43
+ '--no-audit',
44
+ ];
45
+ const p = spawn(cmd, args, {
46
+ cwd: nodejsDir,
47
+ });
48
+ p.on('close', (code) => {
49
+ if (code !== 0) {
50
+ const msg = `${cmd} ${args.join(' ')} in ${nodejsDir} exited with code ${code}.`;
51
+ return reject(new Error(msg));
52
+ }
53
+ return resolve();
54
+ });
55
+ });
56
+ const filesToAdd = await glob(`**`, {
57
+ cwd: layerDir,
58
+ nodir: true,
59
+ });
60
+ const zipfile = new ZipFile();
61
+ filesToAdd.forEach((f) => {
62
+ zipfile.addFile(path.join(layerDir, f), f);
63
+ });
64
+ const zipFileName = await new Promise((resolve) => {
65
+ const zipFileName = path.join(process.cwd(), 'dist', 'layers', `${id}.zip`);
66
+ zipfile.outputStream
67
+ .pipe(createWriteStream(zipFileName))
68
+ .on('close', () => {
69
+ resolve(zipFileName);
70
+ });
71
+ zipfile.end();
72
+ });
73
+ return {
74
+ layerZipFile: zipFileName,
75
+ hash: checkSumOfStrings([
76
+ JSON.stringify(dependencies),
77
+ await checkSumOfFiles([
78
+ packageJSON,
79
+ packageLock,
80
+ // Include this script, so artefact is updated if the way it's built is changed
81
+ fileURLToPath(import.meta.url),
82
+ ]),
83
+ ]),
84
+ };
85
+ };
package/package.json ADDED
@@ -0,0 +1,93 @@
1
+ {
2
+ "name": "@bifravst/aws-cdk-lambda-helpers",
3
+ "version": "1.0.0",
4
+ "description": "Helper functions which simplify working with TypeScript lambdas for AWS CDK.",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "import": "./dist/index.js",
9
+ "node": "./dist/index.js"
10
+ }
11
+ },
12
+ "type": "module",
13
+ "scripts": {
14
+ "test": "tsx --no-warnings --test ./src/*.spec.ts",
15
+ "prepare": "husky",
16
+ "prepublishOnly": "npx tsc --noEmit false --outDir ./dist -d"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/bifravst/aws-cdk-lambda-helpers.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/bifravst/aws-cdk-lambda-helpers/issues"
24
+ },
25
+ "homepage": "https://github.com/bifravst/aws-cdk-lambda-helpers",
26
+ "keywords": [
27
+ "aws",
28
+ "cdk",
29
+ "lambda",
30
+ "typescript"
31
+ ],
32
+ "author": "Nordic Semiconductor ASA | nordicsemi.no",
33
+ "license": "BSD-3-Clause",
34
+ "devDependencies": {
35
+ "@bifravst/eslint-config-typescript": "6.0.17",
36
+ "@bifravst/prettier-config": "1.0.0",
37
+ "@commitlint/config-conventional": "19.1.0",
38
+ "@types/node": "20.12.3",
39
+ "@types/yazl": "2.4.5",
40
+ "husky": "9.0.11",
41
+ "tsx": "4.7.1"
42
+ },
43
+ "lint-staged": {
44
+ "*.ts": [
45
+ "prettier --write",
46
+ "eslint"
47
+ ],
48
+ "*.{md,json,yaml,yml}": [
49
+ "prettier --write"
50
+ ]
51
+ },
52
+ "engines": {
53
+ "node": ">=20",
54
+ "npm": ">=9"
55
+ },
56
+ "release": {
57
+ "branches": [
58
+ "saga",
59
+ {
60
+ "name": "!(saga)",
61
+ "prerelease": true
62
+ }
63
+ ],
64
+ "remoteTags": true,
65
+ "plugins": [
66
+ "@semantic-release/commit-analyzer",
67
+ "@semantic-release/release-notes-generator",
68
+ "@semantic-release/npm",
69
+ [
70
+ "@semantic-release/github",
71
+ {
72
+ "successComment": false,
73
+ "failTitle": false
74
+ }
75
+ ]
76
+ ]
77
+ },
78
+ "publishConfig": {
79
+ "access": "public"
80
+ },
81
+ "files": [
82
+ "package-lock.json",
83
+ "dist",
84
+ "LICENSE",
85
+ "README.md"
86
+ ],
87
+ "prettier": "@bifravst/prettier-config",
88
+ "dependencies": {
89
+ "@swc/core": "1.4.11",
90
+ "glob": "10.3.12",
91
+ "yazl": "2.5.1"
92
+ }
93
+ }