简体   繁体   English

如何获得相对 package.json 依赖项以在 Windows 上使用 AWS 的 sam build 命令?

[英]How do I get relative package.json dependencies to work with AWS's sam build command on Windows?

My goal is to share library code among several of our lambda functions using layers and be able to debug locally and run tests.我的目标是使用层在我们的几个 lambda 函数之间共享库代码,能够在本地调试和运行测试。

npm is able to install dependencies from the local file system. npm 能够从本地文件系统安装依赖项。 When we change our library code, we want all users of that library to get the updated code without having to set up a dedicated npm server.当我们更改库代码时,我们希望该库的所有用户都能获得更新后的代码,而无需设置专用的 npm 服务器。 I'm able to debug locally just fine using the relative paths, but that's before I involve sam build .我可以使用相对路径在本地进行调试,但那是在我涉及sam build之前。 sam build creates a hidden folder at the root of the repository and builds the folder out and eventually runs npm install , however this time the folder structure is different. sam build在存储库的根目录创建一个隐藏文件夹并构建该文件夹并最终运行npm install ,但是这次文件夹结构不同。 The relative paths used in the package.json file are now broken. package.json文件中使用的相对路径现已损坏。 We can't use explicit paths because our repositories reside under our user folders, which are of course different from one developer to another.我们不能使用显式路径,因为我们的存储库位于我们的用户文件夹下,这当然因开发人员而异。

Here's what I did:这是我所做的:

I created a project using sam init and took the defaults (except the name of sam-app-2 ) for a nodejs 12.x project (options 1 and 1 ).我使用sam init创建了一个项目,并为nodejs 12.x项目(选项11 )采用了默认值( sam-app-2的名称除外)。

That command created a folder called sam-app-2 which is the reference for all of the following file names.该命令创建了一个名为sam-app-2的文件夹,它是以下所有文件名的参考。

I created a dependencies/nodejs folder.我创建了一个dependencies/nodejs文件夹。

I added dep.js to that folder:我将dep.js添加到该文件夹:

exports.x = 'It works!!';

I also added package.json to the same folder:我还将package.json添加到同一文件夹中:

{
  "name": "dep",
  "version": "1.0.0",
  "description": "",
  "main": "dep.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Under hello-world (the folder housing the lambda function), I added the following to the dependencies in package.json :在 hello-world(包含 lambda 函数的文件夹)下,我将以下内容添加到package.json中的依赖项中:

"dep": "file:../dependencies/nodejs"

I ran npm install under hello-world and it copied the dependencies under node_modules/dep .我在hello-world下运行了npm install ,它复制了node_modules/dep下的依赖项。 Normally, you would't do that here.通常,您不会在这里这样做。 This is purely to allow me to run locally without involving the sam CLI.这纯粹是为了让我在不涉及 sam CLI 的情况下在本地运行。 It's just pure nodejs code.这只是纯 nodejs 代码。 I can run tests, I can debug and not have to wait twenty seconds or more while sam packages up everything and invokes my function. Developing in this state is awesome because it's very fast.我可以运行测试,我可以调试,而不必等待 20 秒或更长时间,而 sam 将所有内容打包并调用我的 function。在这个 state 中进行开发非常棒,因为它非常快。 However, it'll eventually need to run correctly in the wild.但是,它最终需要在野外正确运行。

I edited ./hello-world/app.js :我编辑了./hello-world/app.js

const dep = require('dep');
let response;

exports.lambdaHandler = async (event, context) => {
    try {
        // const ret = await axios(url);
        response = {
            'statusCode': 200,
            'dep.x': dep.x,
            'body': JSON.stringify({
                message: 'Hello, World!!',
                // location: ret.data.trim()
            })
        }
    } catch (err) {
        console.log(err);
        return err;
    }

    return response
};

if(require.main === module){
    (async () => {
        var result = await exports.lambdaHandler(process.argv[1]);
        console.log(result);
    })();
}

I edited template.yml:我编辑了 template.yml:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app-2

  Sample SAM Template for sam-app-2

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs12.x
      Layers:
        - !Ref DepLayer
      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

  DepLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
        LayerName: sam-app-dependencies-2
        Description: Dependencies for sam app [temp-units-conv]
        ContentUri: ./dependencies/
        CompatibleRuntimes:
          - nodejs12.x
        LicenseInfo: 'MIT'
        RetentionPolicy: Retain
Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

Running it straight from the command line works:直接从命令行运行它:

sam-app-2> node hello-world\app.js
{
  statusCode: 200,
  'dep.x': 'It works!!',
  body: '{"message":"Hello, World!!"}'
}

Even sam deploy works, Yes, it deploys the code to the cloud and when I invoke the lambda function in the cloud.即使sam deploy工作,是的,它将代码部署到云中,当我在云中调用 lambda function 时。 it gives the same result as above.它给出了与上面相同的结果。

However, when I run sam build , it fails with:但是,当我运行sam build时,它失败了:

Building resource 'HelloWorldFunction'
Running NodejsNpmBuilder:NpmPack
Running NodejsNpmBuilder:CopyNpmrc
Running NodejsNpmBuilder:CopySource
Running NodejsNpmBuilder:NpmInstall

Build Failed
Error: NodejsNpmBuilder:NpmInstall - NPM Failed: npm ERR! code ENOLOCAL
npm ERR! Could not install from "..\dependencies\nodejs" as it does not contain a package.json file.

npm ERR! A complete log of this run can be found in:
npm ERR!     C:\Users\Brandon\AppData\Roaming\npm-cache\_logs\2020-03-04T19_34_01_873Z-debug.log

When I try to invoke the lambda locally:当我尝试在本地调用 lambda 时:

sam local invoke "HelloWorldFunction" -e events/event.json
Invoking app.lambdaHandler (nodejs12.x)
DepLayer is a local Layer in the template
Building image...
Requested to skip pulling images ...

Mounting C:\Users\Brandon\source\repos\sam-app-2\hello-world as /var/task:ro,delegated inside runtime container
2020-03-03T19:34:28.824Z        undefined       ERROR   Uncaught Exception      {"errorType":"Runtime.ImportModuleError","errorMessage":"Error: Cannot find module 'dep'\nRequire stack:\n- /var/task/app.js\n- /var/runtime/UserFunction.js\n- /var/runtime/index.js","stack":["Runtime.ImportModuleError: Error: Cannot find module 'dep'","Require stack:","- /var/task/app.js","- /var/runtime/UserFunction.js","- /var/runtime/index.js","    at _loadUserApp (/var/runtime/UserFunction.js:100:13)","    at Object.module.exports.load (/var/runtime/UserFunction.js:140:17)","    at Object.<anonymous> (/var/runtime/index.js:43:30)","    at Module._compile (internal/modules/cjs/loader.js:955:30)","    at Object.Module._extensions..js (internal/modules/cjs/loader.js:991:10)","    at Module.load (internal/modules/cjs/loader.js:811:32)","    at Function.Module._load (internal/modules/cjs/loader.js:723:14)","    at Function.Module.runMain (internal/modules/cjs/loader.js:1043:10)","    at internal/main/run_main_module.js:17:11"]}
?[32mSTART RequestId: b6f39717-746d-1597-9838-3b6472ec8843 Version: $LATEST?[0m
?[32mEND RequestId: b6f39717-746d-1597-9838-3b6472ec8843?[0m
?[32mREPORT RequestId: b6f39717-746d-1597-9838-3b6472ec8843     Init Duration: 237.77 ms        Duration: 3.67 ms       Billed Duration: 100 ms Memory Size: 128 MB     Max Memory Used: 38 MB  ?[0m

{"errorType":"Runtime.ImportModuleError","errorMessage":"Error: Cannot find module 'dep'\nRequire stack:\n- /var/task/app.js\n- /var/runtime/UserFunction.js\n- /var/runtime/index.js"}

When I try to start the API locally with sam local start-api , it fails with the same error as above.当我尝试使用sam local start-api在本地启动 API 时,它失败并出现与上述相同的错误。

I'm thinking that if it weren't for the relative file paths being off during the build phase, I'd be able to have my cake (debugging locally very quickly) and eat it too (run sam build , sam local start-api ).我在想,如果不是因为在构建阶段关闭了相对文件路径,我就可以吃蛋糕(非常快速地在本地调试)并吃掉它(运行sam buildsam local start-api )。

What should I do?我应该怎么办?

I also faced the same issue and eventually came up with an alternative solution to yours.我也遇到了同样的问题,最终想出了一个替代解决方案。 It offers the developer experience you are looking for but avoids the slight inconvenience and maintenance overhead in your solution (aka using the ternary operator for every import that access the shared layer).它提供了您正在寻找的开发人员体验,但避免了解决方案中的轻微不便和维护开销(也就是对访问共享层的每个导入使用三元运算符)。 My proposed solution uses a similar approach as yours but only requires an one-time initialization call per lambda function. Under the hood, it uses module-alias to resolve the dependencies during runtime.我提出的解决方案使用与您的类似的方法,但只需要按 lambda function 进行一次初始化调用。在幕后,它使用模块别名在运行时解决依赖关系。

Here's a link to a repository with an example template: https://github.com/dangpg/aws-sam-shared-layers-template这是带有示例模板的存储库链接: https://github.com/dangpg/aws-sam-shared-layers-template

( tl;dr ) When using the linked template you get: ( tl; dr ) 使用链接模板时,您将获得:

  • Share common code or dependencies among multiple lambda functions using layers使用层在多个 lambda 函数之间共享公共代码或依赖项
  • Does not use any module bundlers (eg, Webpack)不使用任何模块捆绑器(例如,Webpack)
  • Instead, uses module-alias to resolve dependencies during runtime相反,使用模块别名在运行时解决依赖关系
  • Supports local debugging within VSCode via AWS Toolkit支持通过AWS 工具包在 VSCode 中进行本地调试
  • Code can be executed outside of AWS sandbox ( node app.js )代码可以在 AWS 沙箱之外执行 ( node app.js )
  • Supports unit testing with Jest支持使用 Jest 进行单元测试
  • Intellisense/Autocomplete within VSCode VSCode 中的智能感知/自动完成
  • Compatible with SAM CLI ( sam build , sam deploy , sam local invoke , sam local start-api , etc.).兼容 SAM CLI( sam buildsam deploysam local invokesam local start-api等)。
  • Can be deployed and run in the cloud as generic lambdas可以作为通用 lambda 在云中部署和运行

1. Folder structure 1.文件夹结构

+ lambdas
|  + func1
|    - app.js
|    - package.json
|  + func2
|    - app.js
|    - package.json
+ layers
|  + common            // can be any name, I chose common
|    + nodejs          // needs to be nodejs due to how SAM handles layers
|      - index.js      // file for own custom code that you want to share
|      - package.json  // list any dependencies you want to share
- template.yaml

Here's the folder structure I ended up with.这是我最终得到的文件夹结构。 Keep in mind though, that it is quite flexible and doesn't require hard rules in order to fulfill possible relative file paths (however, you would need to adapt some files if your structure differs).但请记住,它非常灵活,不需要硬性规则来满足可能的相对文件路径(但是,如果您的结构不同,您将需要调整一些文件)。

If you want to share npm packages among lambda functions, just add them to the package.json within the layer folder:如果要在lambda函数之间共享npm包,只需将它们添加到层文件夹中的package.json

{
  "name": "common",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "lorem-ipsum": "^2.0.4"
  }
}

For completeness, here's the content of index.js :为了完整起见,这里是index.js的内容:

exports.ping = () => {
  return "pong";
};

2. Template.yaml 2.模板.yaml

[...]
Globals:
  Function:
    Timeout: 3
    Runtime: nodejs14.x
    Environment:
      Variables:
        AWS: true

Resources:
  Func1:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: lambdas/func1/
      Handler: app.lambdaHandler
      Layers:
        - !Ref CommonLayer
  [...]

  Func2:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: lambdas/func2/
      Handler: app.lambdaHandler
      Layers:
        - !Ref CommonLayer
  [...]

  CommonLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      ContentUri: ./layers/common/
      CompatibleRuntimes:
        - nodejs14.x
      RetentionPolicy: Retain

Pretty straightforward, just follow the official example on how to include layers in your lambdas.非常简单,只需按照有关如何在 lambda 中包含层的官方示例进行操作。 Like in your solution, add a gloval env variable, so we can differentiate if we are running code within an AWS sandbox or not.就像在您的解决方案中一样,添加一个 gloval env 变量,这样我们就可以区分我们是否在 AWS 沙箱中运行代码。

3. Lambda package.json 3. Lambda package.json

Add module-alias as dependency and your local common folder as devDependency to each lambda function:module-alias作为dependency项添加,并将本地公共文件夹作为devDependency到每个 lambda function:

...
  "dependencies": {
    "module-alias": "^2.2.2"
  },
  "devDependencies": {
    "common__internal": "file:../../layers/common/nodejs"  // adapt relative path according to your folder structure
  },
...

We will need the local reference to our common folder later on (eg for testing).稍后我们将需要对公共文件夹的本地引用(例如用于测试)。 We add it as devDependency since we only need it for local development and so we don't run into issues when running sam build (since it ignores devDependencies).我们将其添加为 devDependency,因为我们只需要它用于本地开发,因此我们在运行sam build时不会遇到问题(因为它忽略了 devDependencies)。 I chose common__internal as package name, however you are free to choose whatever you like.我选择common__internal作为 package 名称,但是您可以自由选择任何您喜欢的名称。 Make sure to run npm install before doing any local development.在进行任何本地开发之前,请确保运行npm install

4. Lambda handler 4. Lambda 处理程序

Within your handler source code, before you import any packages from your shared layer, initialize module-alias to do the following:在您的处理程序源代码中,在您从共享层导入任何包之前,初始化module-alias以执行以下操作:

const moduleAlias = require("module-alias");

moduleAlias.addAlias("@common", (fromPath, request) => {
  if (process.env.AWS && request.startsWith("@common/")) {
    // AWS sandbox and reference to dependency
    return "/opt/nodejs/node_modules";
  }

  if (process.env.AWS) {
    // AWS sandbox and reference to custom shared code
    return "/opt/nodejs";
  }

  if (request.startsWith("@common/")) {
    // Local development and reference to dependency
    return "../../layers/common/nodejs/node_modules";
  }

  // Local development and reference to custom shared code
  return "../../layers/common/nodejs";
});

const { ping } = require("@common");                     // your custom shared code
const { loremIpsum } = require("@common/lorem-ipsum");   // shared dependency

exports.lambdaHandler = async (event, context) => {
  try {
    const response = {
      statusCode: 200,
      body: JSON.stringify({
        message: "hello world",
        ping: ping(),
        lorem: loremIpsum(),
      }),
    };

    return response;
  } catch (err) {
    console.log(err);
    return err;
  }
};

if (require.main === module) {
  (async () => {
    var result = await exports.lambdaHandler(process.argv[1]);
    console.log(result);
  })();
}

You can move the module-alias code part to a separate file and just import that one in the beginning instead (or even publish your own custom package that you can then properly import; this way you can reference it within each of your lambda function and don't have to duplicate code).您可以将module-alias代码部分移动到一个单独的文件中,然后只在开头导入那个文件(或者甚至发布您自己的自定义 package 然后您可以正确导入;这样您就可以在每个 lambda function 和不必重复代码)。 Again, adjust the relative file paths according to your folder structure.同样,根据您的文件夹结构调整相对文件路径。 Similar to your approach, it checks for the AWS environment variable and adjusts the import paths accordingly.与您的方法类似,它检查AWS环境变量并相应地调整导入路径。 However, this only has to be done once.但是,这只需执行一次。 After that, all successive imports can just use your defined alias: const { ping } = require("@common");之后,所有连续导入都可以使用您定义的别名: const { ping } = require("@common"); and const { loremIpsum } = require("@common/lorem-ipsum");const { loremIpsum } = require("@common/lorem-ipsum"); . . Also here, feel free to define your very own custom logic on how to handle aliases.同样在这里,您可以随意定义您自己的关于如何处理别名的自定义逻辑。 This is just the solution I came up with that worked for me.这只是我想出的对我有用的解决方案。

From this point on, you should be able to execute your lambda code either locally through node app.js or through the SAM CLI ( sam build , sam local invoke , etc.).从现在开始,您应该能够通过node app.js在本地或通过 SAM CLI( sam buildsam local invoke等)在本地执行您的 lambda 代码。 However, if you want local testing and intellisense, there is some additional work left.但是,如果您想要本地测试和智能感知,还有一些额外的工作要做。

5. Intellisense 5.智能感知

For VSCode, you can just add a jsconfig.json file with the respective path mappings for your defined alias.对于 VSCode,您只需添加一个jsconfig.json文件,其中包含您定义的别名的相应路径映射。 Point it to the internal devdependency from earlier:将它指向之前的内部 devdependency:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@common": ["./node_modules/common__internal/index.js"],
      "@common/*": ["./node_modules/common__internal/node_modules/*"]
    }
  }
}

6. Testing 6. 测试

For testing, I personally use Jest.对于测试,我个人使用 Jest。 Fortunately, Jest provides the option to provide path mappings too:幸运的是,Jest 也提供了提供路径映射的选项

// within your lambda package.json
  "jest": {
    "moduleNameMapper": {
      "@common/(.*)": "<rootDir>/node_modules/common__internal/node_modules/$1",
      "@common": "<rootDir>/node_modules/common__internal/"
    }
  }

Final disclaimer最终免责声明

This solution currently only works when using the CommonJS module system.该解决方案目前仅在使用 CommonJS 模块系统时有效。 I haven't been able to reproduce the same result when using ES modules (mostly due to the lacking support of module-alias ).使用 ES 模块时,我无法重现相同的结果(主要是由于缺乏module-alias的支持)。 However, if somebody can come up with a solution using ES modules, I am happy to hear it!然而,如果有人能想出一个使用 ES 模块的解决方案,我很高兴听到!

And that's it.就是这样。 Hopefully I didn't leave anything out, Overall.总的来说,希望我没有遗漏任何东西。 I'm pretty happy with the developer experience this solution is offering.我对该解决方案提供的开发人员体验非常满意。 Feel free to look at the linked template repository for more details.请随时查看链接的模板存储库以获取更多详细信息。 I know it's been a bit since your original question but I left it here in the hope that it will maybe help fellow developers too.我知道自您最初提出问题以来已经有一段时间了,但我将其留在此处,希望它也能对其他开发人员有所帮助。 Cheers干杯

After much frustration and angst, this has been produced: https://github.com/blmille1/aws-sam-layers-template经过多次挫折和焦虑,这已经产生: https : //github.com/blmille1/aws-sam-layers-template

Enjoy!享受!

I followed up on your approved answer and I believe I figured out the correct answer (and posting here since I got here via Google and others may wander in here).我跟进了你批准的答案,我相信我找到了正确的答案(并且在我通过谷歌到达这里后发布在这里,其他人可能会在这里徘徊)。

1. Organize your module in the following way ( my-layers and my-module can be adjusted but the nodejs/node_modules must remain) 1. 按照以下方式组织你的模块my-layersmy-module可以调整,但nodejs/node_modules必须保留)

+ my-layers
| + my-module
|   + nodejs
|     + node_modules
|       + my-module
|         + index.js
|         + package.json
+ my-serverless
  + template.yaml
  + package.json

I don't know the ideal setup for package.json.我不知道 package.json 的理想设置。 Specifying "main": "index.js" was enough for me.指定"main": "index.js"对我来说就足够了。

{
  "name": "my-module",
  "version": "1.0.0",
  "main": "index.js"
}

and this is the index.js这是index.js

exports.ping = () => console.log('pong');

2. In the SAM template lambda layer point to the ../my-layers/ 2. 在 SAM 模板 lambda 层中指向../my-layers/

  MyModuleLayer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: my-module
      ContentUri: ../my-layers/my-module/
      CompatibleRuntimes:
        - nodejs14.x

So far so good.到现在为止还挺好。 But now - to get the code completion and get rid of the crazy require(process.env.AWS ? '/opt/nodejs/common' : '../../layers/layer1/nodejs/common');但是现在 - 要完成代码并摆脱疯狂的require(process.env.AWS ? '/opt/nodejs/common' : '../../layers/layer1/nodejs/common'); you had in the func2internals.js你在func2internals.js

3. Add the dependency to the my-serverless 3.在my-serverless中添加依赖

a.一种。 either install from CLI:从 CLI 安装:

npm i --save ../my-layers/my-module/nodejs/node_modules/my-module

b.or add in package.json dependencies或添加 package.json 依赖项

  "dependencies": {
    "my-module": "file:../my-layers/my-module/nodejs/node_modules/my-module",
  }

4. Use my-module in your serverless function 4. 在你的无服务器功能中使用 my-module

var myModule = require('my-module');

exports.handler = async (event) => {
  myModule.ping();
};

That's it.就是这样。 You have code completion and it works on local env and in sam local invoke and in sam local start-api你有代码完成,它适用于本地环境和sam local invokesam local start-api

And dont forgot to exclude my-layers/my-module/nodejs/node_modules from .gitignore :)并且不要忘记从.gitignore排除my-layers/my-module/nodejs/node_modules :)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 AWS CDK 代码管道开发——NodejsFunction Package.json 依赖项如何安装? - AWS CDK Code Pipeline Development - How Do I Install NodejsFunction Package.json Dependencies? 使用SAM时AWS sam build和sam package有什么区别? - What's the difference between AWS sam build and sam package when using SAM? 如何构建、package 和部署 AWS SAM lambda function 从 azure Devops CI/CD 管道到 AWS - how to build, package and deploy AWS SAM lambda function of python from azure Devops CI/CD pipeline to AWS AWS 无服务器应用程序模型 (SAM) — 如何更改 StageName? - AWS Serverless Application Model (SAM) -- How do I change StageName? 我如何避免 SAM 构建安装 devDependencies - How do i avoid SAM build installing devDependencies 在将 JSON 文件写入 AWS S3 存储桶后,如何让我的文件另存为 JSON - How do I get my file to be saved as JSON after writing a JSON file into AWS S3 bucket 如何使用 AWS SAM esbuild 将 static 文件包含到 lambda package 中? - How to include static files into the lambda package with AWS SAM esbuild? 云构建 angular npm 运行抛出 package.json 错误 - Cloud build angular npm run throws package.json error 如何使用 Node 从 SAM Lambda function 在 S3 中写入 json 文件 - How can I write a json file in S3 from a SAM Lambda function with Node 如何使用 !ImportValue 通过 AWS SAM 模板获取资源 arn - How to use !ImportValue to get the resource arn using AWS SAM template
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM