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

(JS) Expose handler function type to allow manual exporting of Lambda handler #248

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,44 @@ Next, implement your Lambda. Please refer to the [examples](examples/src/main/sc

There are several options to deploy your Lambda. For example you can use the [Lambda console](https://docs.aws.amazon.com/lambda/latest/dg/foundation-console.html), the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html), or the [serverless framework](https://www.serverless.com/framework/docs/providers/aws/guide/deploying).

To deploy a Scala.js Lambda, you will need to know the following:
### CommonJS

To deploy a CommonJS Scala.js Lambda, you will need to know the following:

1. The runtime for your Lambda is Node.js 16.
2. The handler for your Lambda is `index.yourLambdaName`.
- `index` refers to the `index.js` file containing the JavaScript sources for your Lambda.
- `yourLambdaName` is the name of the Scala `object` you created that extends from `IOLambda`.
3. Run `sbt npmPackage` to package your Lambda for deployment. Note that you can currently only have one Lambda per sbt (sub-)project. If you have multiple, you will need to select the one to deploy using `Compile / mainClass := Some("my.lambda.handler")`.
4. For the tooling of your choice, follow their instructions for deploying a Node.js Lambda using the contents of the `target/scala-2.13/npm-package/` directory.

### ES Modules

It's possible to use ES Modules to run your Lambda. You need the following steps instead of the above:

1. The runtime for your Lambda is Node.js 16.
2. Add a `@JSExportTopLevel` inside your lambda object:
```scala
@JSExportTopLevel("yourLambdaName")
val impl: HandlerFn = handlerFn
```
- The type `HandlerFn` is important so Scala.JS will emit your lambda as a JavaScript function.
- `val` is important so your lambda function is emitted as a value, instead of a function returning another function.
- The handler for your lambda is `index.yourLambdaName`
3. Change these settings in your `build.sbt`:

```scala
import org.scalajs.linker.interface.OutputPatterns
import _root_.io.circe.syntax._

scalaJSUseMainModuleInitializer := false
scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule))
npmPackageAdditionalNpmConfig := Map("type" -> "module".asJson)
hugo-vrijswijk marked this conversation as resolved.
Show resolved Hide resolved
```

4. Run `sbt npmPackage` to package your Lambda for deployment. You can have multiple lambda's by exporting multiple handler functions.
5. For the tooling of your choice, follow their instructions for deploying a Node.js Lambda using the contents of the `target/scala-2.13/npm-package/` directory.

As the feral project develops, one of the goals is to provide an sbt plugin that simplifies and automates the deployment process. If this appeals to you, please contribute feature requests, ideas, and/or code!

## Why go feral?
Expand Down
25 changes: 23 additions & 2 deletions lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,37 @@ import io.circe.scalajs._
import scala.scalajs.js
import scala.scalajs.js.JSConverters._
import scala.scalajs.js.|
import scala.util.Try

private[lambda] trait IOLambdaPlatform[Event, Result] {
this: IOLambda[Event, Result] =>

final def main(args: Array[String]): Unit =
/**
* Lambda handler. Implement this type with a val and a call to `handlerFn` to export your
* handler.
*
* @example
* {{{
* val handler: HandlerFn = handlerFn
* }}}
*/
final type HandlerFn = js.Function2[js.Any, facade.Context, js.Promise[js.Any | Unit]]
hugo-vrijswijk marked this conversation as resolved.
Show resolved Hide resolved

final def main(args: Array[String]): Unit = {
// `exports` will throw a ReferenceError in ESModule (because `exports` comes from commonjs modules)
def isESModule = Try(js.Dynamic.global.exports).isFailure

if (isESModule) {
throw new Error(
s"Cannot run ES Module with main module. Use `@JSExportTopLevel` or CommonJS instead. See https://github.com/typelevel/feral#es-modules")
}

js.Dynamic.global.exports.updateDynamic(handlerName)(handlerFn)
}

protected def handlerName: String = getClass.getSimpleName.init

private lazy val handlerFn
final protected lazy val handlerFn
: js.Function2[js.Any, facade.Context, js.Promise[js.Any | Unit]] = {
(event: js.Any, context: facade.Context) =>
(for {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import _root_.io.circe.syntax._

scalaVersion := "2.13.8"
enablePlugins(LambdaJSPlugin)
scalaJSUseMainModuleInitializer := false
scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule))
npmPackageAdditionalNpmConfig := Map("type" -> "module".asJson)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.7.1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("org.typelevel" % "sbt-feral-lambda" % sys.props("plugin.version"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import cats.effect._
import feral.lambda._
import scala.scalajs.js.annotation._

object mySimpleHandler extends IOLambda.Simple[Unit, INothing] {
def apply(event: Unit, context: Context[IO], init: Init): IO[Option[INothing]] = IO.none

@JSExportTopLevel("exportedHandler")
val impl: HandlerFn = handlerFn
}
4 changes: 4 additions & 0 deletions sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-export/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
> npmPackage
$ exists target/scala-2.13/npm-package/index.js
$ exists target/scala-2.13/npm-package/package.json
$ exec node test-export.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if (typeof (await import('./target/scala-2.13/npm-package/index.js')).exportedHandler === 'function') {
process.exit(0);
} else {
process.exit(1);
}