Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(aws-lambda-nodejs): export esbuild asset code bundler #14215

Open
1 of 2 tasks
skyrpex opened this issue Apr 16, 2021 · 7 comments
Open
1 of 2 tasks

(aws-lambda-nodejs): export esbuild asset code bundler #14215

skyrpex opened this issue Apr 16, 2021 · 7 comments
Labels
@aws-cdk/aws-lambda-nodejs effort/small Small work item – less than a day of effort feature-request A feature should be added or improved. p2

Comments

@skyrpex
Copy link
Contributor

skyrpex commented Apr 16, 2021

Can't use aws-lambda-nodejs' capabilities in Lambda@Edge.

Use Case

If you want to use a NodeJS lambda in edge, you need to bundle the code by yourself. It'd be ideal to export the esbuild bundling functionality so anybody can create the AssetCode with esbuild and use it on any other lambda functions such as Lambda@Edge.

Proposed Solution

Just exporting the class Bundling in @aws-cdk/aws-lambda-nodejs would be enough. As a workaround, I did this, which is heavily based on the current implementation but uses esbuild's JS API:

import * as lambda from "@aws-cdk/aws-lambda";
import * as cdk from "@aws-cdk/core";
import * as esbuild from "esbuild";
import findUp from "find-up";
import * as path from "path";

const findDepsLockFilePath = (input: string) => {
  const lockFilePath = findUp.sync(["package-lock.json", "yarn.lock"], {
    cwd: input,
  });

  if (!lockFilePath) {
    throw new Error("Couldn't find a lock file (package-lock.json or yarn.lock)");
  }

  return lockFilePath;
};

/**
 * Converts a Lambda runtime to an esbuild node target.
 */
function toTarget(runtime: lambda.Runtime): string {
  const match = runtime.name.match(/nodejs(\d+)/);

  if (!match) throw new Error("Cannot extract version from runtime.");

  return `node${match[1]}`;
}

export interface EsbuildBundlingProps {
  input: string;
  runtime: lambda.Runtime;
  external?: string[];
  define?: {
    [key: string]: string;
  };
}

export class EsbuildBundling {
  public static bundle(options: EsbuildBundlingProps): lambda.AssetCode {
    const depsLockFilePath = findDepsLockFilePath(options.input);

    return lambda.Code.fromAsset(path.dirname(depsLockFilePath), {
      assetHashType: cdk.AssetHashType.OUTPUT,
      bundling: new EsbuildBundling(options),
    });
  }

  public readonly image: cdk.DockerImage;

  public readonly local: cdk.ILocalBundling;

  constructor(private readonly props: EsbuildBundlingProps) {
    this.image = cdk.DockerImage.fromRegistry("dummy");
    this.local = {
      tryBundle(outputDir) {
        console.log({ outputDir, input: props.input });
        esbuild.buildSync({
          entryPoints: [props.input],
          bundle: true,
          platform: "node",
          target: toTarget(props.runtime),
          outfile: `${outputDir}/index.js`,
          external: ["aws-sdk", ...(props.external || [])],
          define: props.define,
          // minify: true,
        });
        return true;
      },
    };
  }
}

Other

  • 👋 I may be able to implement this feature request
  • ⚠️ This feature might incur a breaking change

This is a 🚀 Feature Request

@skyrpex skyrpex added feature-request A feature should be added or improved. needs-triage This issue or PR still needs to be triaged. labels Apr 16, 2021
@p0wl
Copy link

p0wl commented Apr 16, 2021

This is a nice idea and could also be helpful if you are building your own layers, where you want to bundle your code the same way you do in lambda!

@eladb
Copy link
Contributor

eladb commented May 2, 2021

@jogold I think @skyrpex's idea makes sense - basically extracting the LambdaNodejs bundling logic. WDTY?

@eladb eladb added effort/small Small work item – less than a day of effort p1 labels May 2, 2021
@eladb eladb removed their assignment May 2, 2021
@eladb eladb added p2 and removed p1 labels May 2, 2021
@eladb
Copy link
Contributor

eladb commented May 2, 2021

I am unassigning and marking this issue as p2, which means that we are unable to work on this immediately.

We use +1s to help prioritize our work, and are happy to revaluate this issue based on community feedback. You can reach out to the cdk.dev community on Slack to solicit support for reprioritization.

@hugomallet
Copy link

I don't really understand why it's low priority. How do you guys do with v2 ? Any workaround ?
In the meantime can you at least fix this please ? #15661 (comment)
Thanks

@spencerbeggs
Copy link

spencerbeggs commented Jul 28, 2022

Exporting the Bundling class would, indeed, be enough. I was able to compile TS into Lambda@Edge functions by stiching @skyrpex's example with something else I found online but misplaced the link for. Anyway it looks like this, as a start:

import path from "node:path";
import { AssetHashType, DockerImage, ILocalBundling } from "aws-cdk-lib";
import { experimental } from "aws-cdk-lib/aws-cloudfront";
import { FunctionOptions, Runtime, Code, AssetCode } from "aws-cdk-lib/aws-lambda";
import { Construct } from "constructs";
import { buildSync } from "esbuild";
import { findUpSync } from "find-up";

const findDepsLockFilePath = (input: string) => {
	const lockFilePath = findUpSync(["package-lock.json", "yarn.lock"], {
		cwd: input
	});

	if (!lockFilePath) {
		throw new Error("Couldn't find a lock file (package-lock.json or yarn.lock)");
	}

	return lockFilePath;
};

/**
 * Converts a Lambda runtime to an esbuild node target.
 */
function toTarget(runtime: Runtime): string {
	const match = runtime.name.match(/nodejs(\d+)/);

	if (!match) throw new Error("Cannot extract version from runtime.");

	return `node${match[1]}`;
}

export interface EsbuildBundlingProps {
	input: string;
	runtime: Runtime;
	external?: string[];
	define?: {
		[key: string]: string;
	};
}

export class EsbuildBundling {
	public static bundle(options: EsbuildBundlingProps): AssetCode {
		const depsLockFilePath = findDepsLockFilePath(options.input);

		return Code.fromAsset(path.dirname(depsLockFilePath), {
			assetHashType: AssetHashType.OUTPUT,
			bundling: new EsbuildBundling(options)
		});
	}

	public readonly image: DockerImage;

	public readonly local: ILocalBundling;

	constructor(private readonly props: EsbuildBundlingProps) {
		this.image = DockerImage.fromRegistry("dummy");
		this.local = {
			tryBundle(outputDir) {
				//console.log({ outputDir, input: props.input }, props);
				buildSync({
					entryPoints: [props.input],
					bundle: true,
					platform: "node",
					target: toTarget(props.runtime),
					outfile: `${outputDir}/index.js`,
					external: ["aws-sdk", ...(props.external || [])],
					define: props.define,
					minify: true
				});
				return true;
			}
		};
	}
}

/**
 * environment variables are not supported for Lambda@Edge
 */
export interface NodejsEdgeFunctionProps extends Omit<FunctionOptions, "environment"> {
	/**
	 * Path to the entry file (JavaScript or TypeScript).
	 *
	 * @default - Derived from the name of the defining file and the construct's id.
	 * If the `NodejsFunction` is defined in `stack.ts` with `my-handler` as id
	 * (`new NodejsFunction(this, 'my-handler')`), the construct will look at `stack.my-handler.ts`
	 * and `stack.my-handler.js`.
	 */
	readonly entry?: string;

	/**
	 * The name of the exported handler in the entry file.
	 *
	 * @default handler
	 */
	readonly handler?: string;

	/**
	 * The runtime environment. Only runtimes of the Node.js family are
	 * supported.
	 *
	 * @default Runtime.NODEJS_14_X
	 */
	readonly runtime?: Runtime;

	/**
	 * Whether to automatically reuse TCP connections when working with the AWS
	 * SDK for JavaScript.
	 *
	 * This sets the `AWS_NODEJS_CONNECTION_REUSE_ENABLED` environment variable
	 * to `1`.
	 *
	 * @see https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/node-reusing-connections.html
	 *
	 * @default true
	 */
	readonly awsSdkConnectionReuse?: boolean;

	/**
	 * The path to the dependencies lock file (`yarn.lock` or `package-lock.json`).
	 *
	 * This will be used as the source for the volume mounted in the Docker
	 * container.
	 *
	 * Modules specified in `nodeModules` will be installed using the right
	 * installer (`npm` or `yarn`) along with this lock file.
	 *
	 * @default - the path is found by walking up parent directories searching for
	 *   a `yarn.lock` or `package-lock.json` file
	 */
	readonly depsLockFilePath?: string;

	/**
	 * Bundling options
	 *
	 * @default - use default bundling options: no minify, no sourcemap, all
	 *   modules are bundled.
	 */
	readonly bundling?: EsbuildBundlingProps;

	/**
	 * The path to the directory containing project config files (`package.json` or `tsconfig.json`)
	 *
	 * @default - the directory containing the `depsLockFilePath`
	 */
	readonly projectRoot?: string;

	/**
	 * The stack ID of Lambda@Edge function.
	 *
	 * @default - `edge-lambda-stack-${region}`
	 * @stability stable
	 */
	readonly stackId?: string;
}

export class NodejsEdgeFunction extends experimental.EdgeFunction {
	constructor(scope: Construct, id: string, props: NodejsEdgeFunctionProps) {
		const handler = props.handler ?? "handler";
		const runtime = props.runtime ?? Runtime.NODEJS_16_X;
		super(scope, id, {
			...props,
			runtime,
			stackId: props.stackId,
			code: EsbuildBundling.bundle({
				...props.bundling
			}),
			handler: `index.${handler}`
		});
	}
}

@martpet
Copy link

martpet commented Sep 14, 2022

How do you guys do with v2 ? Any workaround ?

I used '@mrgrain/cdk-esbuild:

import { TypeScriptCode } from '@mrgrain/cdk-esbuild';

const edgeLambda = new experimental.EdgeFunction(scope, 'EdgeLambda', {
    runtime: Runtime.NODEJS_16_X,
    handler: 'edgeLambda.handler',
    code: new TypeScriptCode(`${__dirname}/handlers/authEdgeLambda.ts`)
});

@github-actions
Copy link

This issue has received a significant amount of attention so we are automatically upgrading its priority. A member of the community will see the re-prioritization and provide an update on the issue.

@pahud pahud added p2 and removed p1 labels Jun 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/aws-lambda-nodejs effort/small Small work item – less than a day of effort feature-request A feature should be added or improved. p2
Projects
None yet
Development

No branches or pull requests

8 participants