floydspace/serverless-esbuild

Does serverless-esbuild support ESM/ES Modules?

Opened this issue · 3 comments

My code consists of ES Modules, and I use "type": "module" in package.json to make that clear. I do have a few config files (jest, eslint, and prettier) and a script file that are .cjs extensions. They shouldn't execute when a request is sent to my endpoint, however. Everything works until I tried adding serverless-esbuild to my project, at which point I get the below error (when I send a request to the endpoint running offline). Am I doing something wrong?

Error

× Unhandled exception in handler 'get'.
× ReferenceError: module is not defined in ES module scope
  This file is being treated as an ES module because it has a '.js' file extension and 'C:\Users\dwthomps\Documents\projects\omniact\genesys-cloud-admin-portal-audit-api\.esbuild\.build\package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.
      at file:///C:/Users/dwthomps/Documents/projects/omniact/genesys-cloud-admin-portal-audit-api/.esbuild/.build/src/routes/get/handler.js:92:99071
      at ModuleJob.run (node:internal/modules/esm/module_job:193:25)
      at async ESMLoader.import (node:internal/modules/esm/loader:526:24)
      at async importModuleDynamicallyWrapper (node:internal/vm/module:438:15)
      at async _tryAwaitImport (C:\Users\dwthomps\Documents\projects\omniact\genesys-cloud-admin-portal-audit-api\node_modules\.pnpm\serverless-offline@12.0.4_serverless@3.34.0\node_modules\serverless-offline\src\lambda\handler-runner\in-process-runner\aws-lambda-ric\UserFunction.js:215:14)
      at async _tryRequire (C:\Users\dwthomps\Documents\projects\omniact\genesys-cloud-admin-portal-audit-api\node_modules\.pnpm\serverless-offline@12.0.4_serverless@3.34.0\node_modules\serverless-offline\src\lambda\handler-runner\in-process-runner\aws-lambda-ric\UserFunction.js:275:24)
      at async _loadUserApp (C:\Users\dwthomps\Documents\projects\omniact\genesys-cloud-admin-portal-audit-api\node_modules\.pnpm\serverless-offline@12.0.4_serverless@3.34.0\node_modules\serverless-offline\src\lambda\handler-runner\in-process-runner\aws-lambda-ric\UserFunction.js:304:14)
      at async module.exports.load (C:\Users\dwthomps\Documents\projects\omniact\genesys-cloud-admin-portal-audit-api\node_modules\.pnpm\serverless-offline@12.0.4_serverless@3.34.0\node_modules\serverless-offline\src\lambda\handler-runner\in-process-runner\aws-lambda-ric\UserFunction.js:341:21)
      at async InProcessRunner.run (file:///C:/Users/dwthomps/Documents/projects/omniact/genesys-cloud-admin-portal-audit-api/node_modules/.pnpm/serverless-offline@12.0.4_serverless@3.34.0/node_modules/serverless-offline/src/lambda/handler-runner/in-process-runner/InProcessRunner.js:41:21)
× module is not defined in ES module scope
  This file is being treated as an ES module because it has a '.js' file extension and 'C:\Users\dwthomps\Documents\projects\omniact\genesys-cloud-admin-portal-audit-api\.esbuild\.build\package.json' contains "type": "module". To treat it as a CommonJS script, rename it to use the '.cjs' file extension.

serverless.yaml

service: genesys-cloud-admin-portal-audit-api

plugins:
  - serverless-esbuild
  - serverless-dotenv-plugin
  - serverless-offline

custom:
  serverless-offline:
    httpPort: ${env:PORT, 4000}
    noTimeout: -t
    reloadHandler: true
  esbuild:
   bundle: true
   minify: true
   external:
     - aws-sdk

provider:
  name: aws
  runtime: nodejs18.x
  lambdaHashingVersion: 20201221
  memorySize: 128
  timeout: 30
  environment:
    NODE_OPTIONS:  -r ./deploy/openTelemetryProvider.cjs

useDotenv: true
package:
  individually: true

functions:
  get:
    handler: ./src/routes/get/handler.getHandler
    events:
      - http:
          path: /api/audit
          method: get
          response:
            headers:
              Content-Type: "'application/json'"

package.json

{
	"name": "my-project",
	"version": "0.0.1",
	"type": "module",
	"scripts": {
		"dev": "sls offline --noPrependStageInUrl --reloadHandler",
		"dev:cached": "sls offline --allowCache --noPrependStageInUrl",
		"dev:debug": "node --inspect ./node_modules/serverless/bin/serverless.js offline",
		"sls:invoke": "sls invoke local --function",
		"test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
		"test:integration": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --testMatch=**/*.integration.test.js --detectOpenHandles",
		"test:watch": "jest --watch --verbose",
		"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
		"test:debug-watch": "node --inspect-brk node_modules/.bin/jest --runInBand --watch",
		"coverage": "jest --coverage",
		"format": "npm run lint -- --fix && npm run prettier -- --write",
		"prettier": "prettier ./src",
		"lint": "eslint ./src",
		"openapi:build": "swagger-cli bundle -r --outfile ./docs/openapi.json ./openapi/spec.yaml",
		"openapi:serve": "serve -d ./docs",
		"package": "rimraf ./dist && sls package --package ./dist",
		"release": "standard-version"
	},
	"standard-version": {},
	"engines": {
		"node": "16"
	},
	"keywords": [],
	"author": "",
	"license": "ISC",
	"dependencies": {
		"@middy/core": "^4.6.0",
		"@middy/http-error-handler": "^4.6.0",
		"@middy/http-event-normalizer": "^4.6.0",
		"@middy/validator": "4.6.0",
		"@opentelemetry/instrumentation-mongodb": "^0.21.0",
		"@opentelemetry/sdk-node": "0.23.1-alpha.16",
		"@types/node": "^20.2.3",
		"aws-sdk": "^2.1438.0",
		"dotenv": "^16.3.1",
		"envalid": "^7.3.1",
		"mongodb": "^5.7.0",
		"omniact-common-utilities": "^1.0.20"
	},
	"devDependencies": {
		"@apidevtools/swagger-cli": "^4.0.4",
		"@shelf/jest-mongodb": "^4.1.7",
		"babel-jest": "^29.6.2",
		"cross-env": "^7.0.3",
		"eslint": "^8.47.0",
		"eslint-config-prettier": "^9.0.0",
		"eslint-plugin-jest": "^27.2.3",
		"eslint-plugin-jsdoc": "^46.4.6",
		"eslint-plugin-prettier": "^5.0.0",
		"eslint-plugin-unicorn": "^48.0.1",
		"jest": "^29.6.2",
		"pre-commit": "^1.2.2",
		"prettier": "^3.0.2",
		"rimraf": "^5.0.1",
		"serve": "^14.2.0",
		"serverless": "^3.34.0",
		"serverless-dotenv-plugin": "^6.0.0",
		"serverless-esbuild": "^1.46.0",
		"serverless-offline": "^12.0.4"
	},
	"pre-commit": [
		"format",
		"test"
	],
	"lint-staged": {
		"*.js": []
	}
}

src/routes/get/handler.js

import middy from "@middy/core";
import httpErrorHandler from "@middy/http-error-handler";
import httpEventNormalizer from "@middy/http-event-normalizer";
import validatorMiddleware from "@middy/validator";
import { transpileSchema } from "@middy/validator/transpile";
import { validationErrorJSONFormatter } from "../../middleware/validationErrorJSONFormatter.js";
import { validationSchema } from "./validationSchema.js";

export const getHandler = middy()
	.use(httpEventNormalizer()) // parse event json string as object
	.use(httpErrorHandler()) // handle common http errors and returns proper responses
	.use(validationErrorJSONFormatter()) // format response nicely when there is a validation error
	.use(
		validatorMiddleware({
			eventSchema: transpileSchema(validationSchema, { verbose: true }),
		})
	)
	.handler(async (event, context, { signal }) => {
		return {
			statusCode: 200,
			body: 'hello world!'
		};
	});

Looking at this a bit closer, the issue seems to be resolved if I remove type: "module" from the resulting .esbuild/.build/src/routes/get/package.json. Is there an option to do this in my serverless.yaml?

I think I had some trouble with this too. My serverless.yml contains:

custom:
  esbuild:
    format: esm
    outputFileExtension: .mjs
    exclude:
      - "@aws-sdk/*"

And I end up with a .mjs file deployed to Lambda.

I also had to add a banner like evanw/esbuild#1921 (comment) , but that may have been to deal with a not-fully-ESM module I was using.

@timkingman how did you implemented banner? thanks

edit: nvm, found it:

custom:
  esbuild:
    format: esm
    outputFileExtension: .mjs
    banner:
      js: import { createRequire } from 'module';const require = (await import('node:module')).createRequire(import.meta.url);const __filename = (await import('node:url')).fileURLToPath(import.meta.url);const __dirname = (await import('node:path')).dirname(__filename);