floydspace / serverless-esbuild

💨 A Serverless framework plugin to bundle JavaScript and TypeScript with extremely fast esbuild
MIT License
445 stars 138 forks source link

Does serverless-esbuild support ESM/ES Modules? #483

Open capndave opened 1 year ago

capndave commented 1 year ago

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!'
        };
    });
capndave commented 1 year ago

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?

timkingman commented 1 year ago

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 https://github.com/evanw/esbuild/issues/1921#issuecomment-1491470829 , but that may have been to deal with a not-fully-ESM module I was using.

efstathiosntonas commented 1 year ago

@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);