diff --git a/README.md b/README.md index 5c35f7b2..0d929822 100644 --- a/README.md +++ b/README.md @@ -24,11 +24,22 @@ 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: + 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")`. +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 containing a function, instead of a function returning another function. + - 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 export and can be changed if desired. + +3. Run `sbt npmPackage` to package your Lambda for deployment. You can have multiple lambda's per sbt project by exporting multiple handler functions. 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. 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! @@ -37,13 +48,13 @@ As the feral project develops, one of the goals is to provide an sbt plugin that The premise that you can (and should!) write production-ready serverless functions in Scala targeting JavaScript may be a surprising one. This project—and the rapid maturity of the Typelevel.js ecosystem—is motivated by three ideas. -1. **JavaScript is the ideal compile target for serverless functions.** - - There are a lot of reasons for this, cold-start being one of them, but more generally it's important to remember what the JVM is and is not good at. In particular, the JVM excels at long-lived multithreaded applications which are relatively memory-heavy and rely on medium-lifespan heap allocations. So in other words, persistent microservices. +1. **JavaScript is the ideal compile target for serverless functions.** + + There are a lot of reasons for this, cold-start being one of them, but more generally it's important to remember what the JVM is and is not good at. In particular, the JVM excels at long-lived multithreaded applications which are relatively memory-heavy and rely on medium-lifespan heap allocations. So in other words, persistent microservices. - Serverless functions are, by definition, not this. They are not persistent, they are (generally) single-threaded, and they need to start very quickly with minimal warming. They do often apply moderate-to-significant heap pressure, but this factor is more than outweighed by the others. + Serverless functions are, by definition, not this. They are not persistent, they are (generally) single-threaded, and they need to start very quickly with minimal warming. They do often apply moderate-to-significant heap pressure, but this factor is more than outweighed by the others. - V8 (the JavaScript engine in Node.js) is a very good runtime for these kinds of use-cases. Realistically, it may be the best-optimized runtime in existence for these requirements, similar to how the JVM is likely the best-optimized runtime in existence for the persistent microservices case. + V8 (the JavaScript engine in Node.js) is a very good runtime for these kinds of use-cases. Realistically, it may be the best-optimized runtime in existence for these requirements, similar to how the JVM is likely the best-optimized runtime in existence for the persistent microservices case. 2. **Scala.js and Cats Effect work together to provide powerful, well-defined semantics for writing JavaScript applications.** diff --git a/build.sbt b/build.sbt index 0207843f..4b3a7d39 100644 --- a/build.sbt +++ b/build.sbt @@ -16,7 +16,7 @@ name := "feral" -ThisBuild / tlBaseVersion := "0.1" +ThisBuild / tlBaseVersion := "0.2" ThisBuild / startYear := Some(2021) ThisBuild / developers := List( diff --git a/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala b/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala index 08bf57e8..391db1ac 100644 --- a/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala +++ b/lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala @@ -26,12 +26,19 @@ import scala.scalajs.js.| private[lambda] trait IOLambdaPlatform[Event, Result] { this: IOLambda[Event, Result] => - final def main(args: Array[String]): Unit = - js.Dynamic.global.exports.updateDynamic(handlerName)(handlerFn) + /** + * Lambda handler. Implement this type with a val and a call to `handlerFn` to export your + * handler. + * + * @example + * {{{ + * @JSExportTopLevel("handler") + * val handler: HandlerFn = handlerFn + * }}} + */ + final type HandlerFn = js.Function2[js.Any, facade.Context, js.Promise[js.Any | Unit]] - 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 { diff --git a/sbt-lambda/src/main/scala/feral/lambda/sbt/LambdaJSPlugin.scala b/sbt-lambda/src/main/scala/feral/lambda/sbt/LambdaJSPlugin.scala index 8472329b..6ebc1fe7 100644 --- a/sbt-lambda/src/main/scala/feral/lambda/sbt/LambdaJSPlugin.scala +++ b/sbt-lambda/src/main/scala/feral/lambda/sbt/LambdaJSPlugin.scala @@ -38,7 +38,7 @@ object LambdaJSPlugin extends AutoPlugin { override def projectSettings: Seq[Setting[_]] = Seq( libraryDependencies += BuildInfo.organization %%% BuildInfo.name.drop(4) % BuildInfo.version, - scalaJSUseMainModuleInitializer := true, + scalaJSUseMainModuleInitializer := false, scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), npmPackageOutputFilename := "index.js", npmPackageStage := Stage.FullOpt diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/build.sbt b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/build.sbt new file mode 100644 index 00000000..40f4ad3b --- /dev/null +++ b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/build.sbt @@ -0,0 +1,5 @@ +import _root_.io.circe.syntax._ + +scalaVersion := "2.13.8" +enablePlugins(LambdaJSPlugin) +scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule)) diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/project/build.properties b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/project/build.properties new file mode 100644 index 00000000..22af2628 --- /dev/null +++ b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.7.1 diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/project/plugins.sbt b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/project/plugins.sbt new file mode 100644 index 00000000..f878a0fb --- /dev/null +++ b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.typelevel" % "sbt-feral-lambda" % sys.props("plugin.version")) diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/src/main/scala/mySimpleHandler.scala b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/src/main/scala/mySimpleHandler.scala new file mode 100644 index 00000000..358473d4 --- /dev/null +++ b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/src/main/scala/mySimpleHandler.scala @@ -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("mySimpleHandler") + val impl: HandlerFn = handlerFn +} diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/test b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/test new file mode 100644 index 00000000..752897ff --- /dev/null +++ b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/test @@ -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 diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/test-export.mjs b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/test-export.mjs new file mode 100644 index 00000000..0a35dd1e --- /dev/null +++ b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/test-export.mjs @@ -0,0 +1,5 @@ +if (typeof (await import('./target/scala-2.13/npm-package/index.js')).mySimpleHandler === 'function') { + process.exit(0); +} else { + process.exit(1); +} diff --git a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-simple/src/main/scala/mySimpleHandler.scala b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-simple/src/main/scala/mySimpleHandler.scala index 16a54e88..358473d4 100644 --- a/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-simple/src/main/scala/mySimpleHandler.scala +++ b/sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-simple/src/main/scala/mySimpleHandler.scala @@ -1,6 +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("mySimpleHandler") + val impl: HandlerFn = handlerFn }