Building Netlify functions with dependencies

Hello :slight_smile:

I am trying to write and deploy my first Netlify edge function. The goal is to send notifications via Firebase Cloud Messaging. I created a send-notification.js file that requires two npm dependencies.
When running netlify dev everything works fine.
However, when i deploy my app the build fails due to the external dependencies:

3:38:33 PM: Edge Functions bundling                                       
3:38:33 PM: ────────────────────────────────────────────────────────────────
3:38:33 PM: ​
3:38:33 PM: Packaging Edge Functions from netlify/edge-functions directory:
3:38:33 PM:  - send-notifications
3:38:34 PM: error: Uncaught (in promise) Error: Relative import path firebase-admin/messaging not prefixed with / or ./ or ../ and not in import map from file:///root/netlify/edge-functions/send-notifications.js
3:38:34 PM:       const ret = new Error(getStringFromWasm0(arg0, arg1));
3:38:34 PM:                   ^
3:38:34 PM:     at __wbg_new_15d3966e9981a196 (file:///opt/buildhome/node-deps/node_modules/@netlify/edge-bundler/deno/vendor/deno.land/x/eszip@v0.40.0/eszip_wasm.generated.js:417:19)
3:38:34 PM:     at <anonymous> (file:///opt/buildhome/node-deps/node_modules/@netlify/edge-bundler/deno/vendor/deno.land/x/eszip@v0.40.0/eszip_wasm_bg.wasm:1:93412)
3:38:34 PM:     at <anonymous> (file:///opt/buildhome/node-deps/node_modules/@netlify/edge-bundler/deno/vendor/deno.land/x/eszip@v0.40.0/eszip_wasm_bg.wasm:1:1499594)
3:38:34 PM:     at <anonymous> (file:///opt/buildhome/node-deps/node_modules/@netlify/edge-bundler/deno/vendor/deno.land/x/eszip@v0.40.0/eszip_wasm_bg.wasm:1:1938165)
3:38:34 PM:     at __wbg_adapter_40 (file:///opt/buildhome/node-deps/node_modules/@netlify/edge-bundler/deno/vendor/deno.land/x/eszip@v0.40.0/eszip_wasm.generated.js:231:6)
3:38:34 PM:     at real (file:///opt/buildhome/node-deps/node_modules/@netlify/edge-bundler/deno/vendor/deno.land/x/eszip@v0.40.0/eszip_wasm.generated.js:215:14)
3:38:34 PM:     at eventLoopTick (ext:core/01_core.js:182:11)
3:38:34 PM: ​
3:38:34 PM: Bundling of edge function failed                              
3:38:34 PM: ────────────────────────────────────────────────────────────────
3:38:34 PM: ​
3:38:34 PM:   Error message
3:38:34 PM:   It seems like you're trying to import an npm module. This is only supported via CDNs like esm.sh. Have you tried 'import mod from https://esm.sh/firebase-admin/messaging'?
​
3:38:34 PM:   Error location
3:38:34 PM:   While bundling edge function
3:38:34 PM: ​
3:38:34 PM:   Resolved config
3:38:34 PM:   build:
3:38:34 PM:     command: npm install
3:38:34 PM:     commandOrigin: ui
3:38:34 PM:     edge_functions: /opt/build/repo/netlify/edge-functions
3:38:34 PM:     publish: /opt/build/repo
3:38:34 PM:     publishOrigin: ui
3:38:34 PM:   functionsDirectory: /opt/build/repo/netlify/edge-functions
3:38:34 PM:   redirects:
3:38:35 PM: Build failed due to a user error: Build script returned non-zero exit code: 2
3:38:35 PM: Failing build: Failed to build site
3:38:35 PM: Finished processing build request in 13.933s

I also tried to import the dependencies via CDN but without success. Ideally i would like it to work with the dependencies defined in my package.json.

For anyone wondering this is my project setup:
netlify.toml

[build]
    external_node_modules = ["firebase-admin", "dotenv"]
    functions = "netlify/edge-functions/"

[[redirects]]
    from = "/api/*"
    to = "/.netlify/functions/:splat"
    status = 200

My netlify edge function:
netlify/edge-functions/send-notifications.js

import { initializeApp, applicationDefault } from 'firebase-admin/app';
import dotenv from 'dotenv';
import { getMessaging } from 'firebase-admin/messaging';

dotenv.config();
const credentialsPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
console.log(credentialsPath);

initializeApp({
  credential: applicationDefault(),
  projectId: 'shared-list-preview',
});

export async function handler(event, context) {
  console.log('Send notifications called')

  if (event.httpMethod === undefined && event.httpMethod !== 'POST') {
    return {
      statusCode: 400,
      body: JSON.stringify({
        error: 'Only post requests allowed.'
      }),
    };
  } else  {
    const requestBody = JSON.parse(event.body)
    if (isEmpty(requestBody.title) || isEmpty(requestBody.text) || isEmpty(requestBody.fcmTokens)) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          error: 'Body must contain title, text and fcmTokens.'
        }),
      };
    }
  }

  const requestBody = JSON.parse(event.body)
  const message = {
    notification: {
      title: requestBody.title,
      body: requestBody.text,
    },
    tokens: requestBody.fcmTokens
  }

  try {
    const batchResponse = await getMessaging().sendEachForMulticast(message);
    // Return a success response
    if (batchResponse.failureCount === 0) {
      const successMsg = `Successfully sent ${batchResponse.successCount} message(s)`
      console.log(successMsg)
      return {
        statusCode: 200,
        body: JSON.stringify({
          message: successMsg
        }),
      };
    } else {
      const failureMsg = `Failure - Was able to sent ${batchResponse.successCount}/${batchResponse.failureCount} message(s)`
      console.log(failureMsg)
      return {
        statusCode: 500,
        body: JSON.stringify({
          message: failureMsg,
          responses: batchResponse.responses
        }),
      }
    }
  } catch (error) {
    console.log('Error sending message:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({
        error: error.message
      }),
    };
  }
}

function isEmpty(str) {
  return (!str || str.length === 0 );
}

(I get warnings locally, if anyone knows how to get rid of them I would be happy to know:)

β—ˆ Loaded edge function send-notifications
β—ˆ Failed to run Edge Function send-notifications:
TypeError: Relative import path "firebase-admin/messaging" not prefixed with / or ./ or ../ and not in import map from "my-project/netlify/edge-functions/send-notifications.js"
    at my-project/netlify/edge-functions/send-notifications.js:3:30
    at async file:///user/AppData/Local/Temp/tmp-31204-qKAJa6073fym/dev.js:7:35 {
  code: "ERR_MODULE_NOT_FOUND"

I would gladly appreciate any hints! Thank you so much :slight_smile:

You’re trying to use Netlify Functions, but instead using Edge Functions. Simply move it from netlify/edge-functions to netlify/functions.

and remove this.

Hey hrishikesh

Thank you so much for the fast response. That actually fixed the issue!

I have another follow up question, would be glad if you could point me in the right direction there as well!
In my code I am reading a json file from an environment variable:

const credentialsPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;

Currently i have the required json file in the root of my project and this variable points to that file. However, i think it is not deployed with my app:
"errorMessage":"Failed to read credentials from file ./my-credentials-file.json: Error: ENOENT: no such file or directory

My question is - how can I access this file in a netlify function? Or even better - Is there a more secure way to handle this with netlify because i don’t really want that file to be checked in my code.

Why not add the contents of the JSON file to an environment variable and use that value instead? If you absolutely need a JSON file instead of a JSON string, I’d recommend the following approach:

import {writeFileSync} from 'fs'
writeFileSync('/tmp/credentials.json', JSON.stringify({
  key1: process.env.VALUE_1 // add each key as a variable (personal recommendation, but you can choose to save it as one string too)
})) // AWS Lambda only provides write access to `/tmp/`

and then use /tmp/credentials.json as the file path.

The default way is by pointing to the json file but thanks for the hint. I found a way to use a set of environment variables instead. Thank youuu :slight_smile: