Skip to content

Commit

Permalink
feat: adds support for serverless-azure-functions plugin (#446)
Browse files Browse the repository at this point in the history
* feat: adds support for `serverless-azure-functions` plugin

Adds support for Azure Function Apps built in Node via the
`serverless-azure-functions` plugin.

Currently when trying to use the plugin with the `azure`
runtime it does not compile because there is not a function
matcher available, only for AWS and GCP.

This commit adds a matcher for Azure, along with unit tests
and supporting documentation in README.md.

* test: update snapshots for e2e

---------

Co-authored-by: Daniel Waghorn <[email protected]>
  • Loading branch information
danielwaghorn and Daniel Waghorn committed Mar 15, 2023
1 parent 1489525 commit 47569f5
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 6 deletions.
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,9 @@ The following `esbuild` options are automatically set.

## Supported Runtimes

This plugin will automatically set the esbuild `target` for the following supported Serverless runtimes
This plugin will automatically set the esbuild `target` for the following supported Serverless runtimes:

AWS:
###AWS

| Runtime | Target |
| ------------ | -------- |
Expand All @@ -137,6 +137,26 @@ AWS:
| `nodejs14.x` | `node14` |
| `nodejs12.x` | `node12` |

### Azure

This plugin is compatible with the [serverless-azure-functions](https://github.com/serverless/serverless-azure-functions) plugin, and will set the runtimes accordingly.

| Runtime | Target |
| ------------ | -------- |
| `nodejs18` | `node18` |
| `nodejs16` | `node16` |
| `nodejs14` | `node14` |
| `nodejs12` | `node12` |

**Please Note** When using this package in conjunction with the `serverless-azure-functions` plugin, the following additional configuration is required to ensure function apps are built correctly:

```yml
package:
patterns: ["host.json", "**/function.json"],
```

### Non-Node functions

If you wish to use this plugin alongside non Node functions like Python or functions with images, this plugin will automatically ignore any function which does not contain a handler or use a supported Node.js runtime.

_Note:_ If you are using Python functions with Serverless Offline you will need to change the `outputWorkFolder` and `outputBuildFolder` to folder names without fullstops.
Expand Down
1 change: 1 addition & 0 deletions e2e/__snapshots__/complete.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ exports[`complete 5`] = `
"Action": [
"logs:CreateLogStream",
"logs:CreateLogGroup",
"logs:TagResource",
],
"Effect": "Allow",
"Resource": [
Expand Down
1 change: 1 addition & 0 deletions e2e/__snapshots__/individually.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ exports[`individually 6`] = `
"Action": [
"logs:CreateLogStream",
"logs:CreateLogGroup",
"logs:TagResource",
],
"Effect": "Allow",
"Resource": [
Expand Down
1 change: 1 addition & 0 deletions e2e/__snapshots__/minimal.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -14087,6 +14087,7 @@ exports[`minimal 5`] = `
"Action": [
"logs:CreateLogStream",
"logs:CreateLogGroup",
"logs:TagResource",
],
"Effect": "Allow",
"Resource": [
Expand Down
22 changes: 19 additions & 3 deletions src/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,21 +217,29 @@ export type AwsNodeProviderRuntimeMatcher<Versions extends number> = {
[Version in Versions as `nodejs${Version}.x`]: `node${Version}`;
};

export type AzureNodeProviderRuntimeMatcher<Versions extends number> = {
[Version in Versions as `nodejs${Version}`]: `node${Version}`;
};

export type GoogleNodeProviderRuntimeMatcher<Versions extends number> = {
[Version in Versions as `nodejs${Version}`]: `node${Version}`;
};

export type AwsNodeMatcher = AwsNodeProviderRuntimeMatcher<12 | 14 | 16 | 18>;

export type AzureNodeMatcher = AzureNodeProviderRuntimeMatcher<12 | 14 | 16 | 18>;

export type GoogleNodeMatcher = GoogleNodeProviderRuntimeMatcher<12 | 14 | 16 | 18>;

export type NodeMatcher = AwsNodeMatcher & GoogleNodeMatcher;
export type NodeMatcher = AwsNodeMatcher & AzureNodeMatcher & GoogleNodeMatcher;

export type AwsNodeMatcherKey = keyof AwsNodeMatcher;

export type AzureNodeMatcherKey = keyof AzureNodeMatcher;

export type GoogleNodeMatcherKey = keyof GoogleNodeMatcher;

export type NodeMatcherKey = AwsNodeMatcherKey | GoogleNodeMatcherKey;
export type NodeMatcherKey = AwsNodeMatcherKey | AzureNodeMatcherKey | GoogleNodeMatcherKey;

const awsNodeMatcher: AwsNodeMatcher = {
'nodejs18.x': 'node18',
Expand All @@ -240,17 +248,25 @@ const awsNodeMatcher: AwsNodeMatcher = {
'nodejs12.x': 'node12',
};

const azureNodeMatcher: AzureNodeMatcher = {
nodejs18: 'node18',
nodejs16: 'node16',
nodejs14: 'node14',
nodejs12: 'node12',
};

const googleNodeMatcher: GoogleNodeMatcher = {
nodejs18: 'node18',
nodejs16: 'node16',
nodejs14: 'node14',
nodejs12: 'node12',
};

const nodeMatcher: NodeMatcher = { ...googleNodeMatcher, ...awsNodeMatcher };
const nodeMatcher: NodeMatcher = { ...googleNodeMatcher, ...awsNodeMatcher, ...azureNodeMatcher };

export const providerRuntimeMatcher = Object.freeze<Record<string, NodeMatcher>>({
aws: awsNodeMatcher as NodeMatcher,
azure: azureNodeMatcher as NodeMatcher,
google: googleNodeMatcher as NodeMatcher,
});

Expand Down
162 changes: 161 additions & 1 deletion src/tests/helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jest.mock('fs-extra');

const consoleSpy = jest.spyOn(console, 'log').mockImplementation();

afterEach(() => {
afterAll(() => {
jest.resetAllMocks();
});

Expand Down Expand Up @@ -176,6 +176,166 @@ describe('extractFunctionEntries', () => {
expect(consoleSpy).toBeCalled();
});
});

describe('azure', () => {
it('should return entries for handlers which reference files in the working directory', () => {
jest.mocked(fs.existsSync).mockReturnValue(true);
const functionDefinitions = {
function1: {
events: [],
handler: 'file1.handler',
},
function2: {
events: [],
handler: 'file2.handler',
},
};

const fileNames = extractFunctionEntries(cwd, 'azure', functionDefinitions);

expect(fileNames).toStrictEqual([
{
entry: 'file1.ts',
func: functionDefinitions.function1,
functionAlias: 'function1',
},
{
entry: 'file2.ts',
func: functionDefinitions.function2,
functionAlias: 'function2',
},
]);
});

it('should return entries for handlers which reference directories that contain index files', () => {
jest.mocked(fs.existsSync).mockImplementation((fPath) => {
return typeof fPath !== 'string' || fPath.endsWith('/index.ts');
});

const functionDefinitions = {
function1: {
events: [],
handler: 'dir1.handler',
},
function2: {
events: [],
handler: 'dir2.handler',
},
};

const fileNames = extractFunctionEntries(cwd, 'azure', functionDefinitions);

expect(fileNames).toStrictEqual([
{
entry: 'dir1/index.ts',
func: functionDefinitions.function1,
functionAlias: 'function1',
},
{
entry: 'dir2/index.ts',
func: functionDefinitions.function2,
functionAlias: 'function2',
},
]);
});

it('should return entries for handlers which reference files in folders in the working directory', () => {
jest.mocked(fs.existsSync).mockReturnValue(true);
const functionDefinitions = {
function1: {
events: [],
handler: 'folder/file1.handler',
},
function2: {
events: [],
handler: 'folder/file2.handler',
},
};

const fileNames = extractFunctionEntries(cwd, 'azure', functionDefinitions);

expect(fileNames).toStrictEqual([
{
entry: 'folder/file1.ts',
func: functionDefinitions.function1,
functionAlias: 'function1',
},
{
entry: 'folder/file2.ts',
func: functionDefinitions.function2,
functionAlias: 'function2',
},
]);
});

it('should return entries for handlers which reference files using a relative path in the working directory', () => {
jest.mocked(fs.existsSync).mockReturnValue(true);
const functionDefinitions = {
function1: {
events: [],
handler: './file1.handler',
},
function2: {
events: [],
handler: './file2.handler',
},
};

const fileNames = extractFunctionEntries(cwd, 'azure', functionDefinitions);

expect(fileNames).toStrictEqual([
{
entry: 'file1.ts',
func: functionDefinitions.function1,
functionAlias: 'function1',
},
{
entry: 'file2.ts',
func: functionDefinitions.function2,
functionAlias: 'function2',
},
]);
});

it('should return entries for handlers on a Windows platform', () => {
jest.mocked(fs.existsSync).mockReturnValue(true);
jest.spyOn(path, 'relative').mockReturnValueOnce('src\\file1.ts');
jest.spyOn(os, 'platform').mockReturnValueOnce('win32');
const functionDefinitions = {
function1: {
events: [],
handler: 'file1.handler',
},
};

const fileNames = extractFunctionEntries(cwd, 'azure', functionDefinitions);

expect(fileNames).toStrictEqual([
{
entry: 'src/file1.ts',
func: functionDefinitions.function1,
functionAlias: 'function1',
},
]);
});

it('should throw an error if the handlers reference a file which does not exist', () => {
jest.mocked(fs.existsSync).mockReturnValue(false);
const functionDefinitions = {
function1: {
events: [],
handler: 'file1.handler',
},
function2: {
events: [],
handler: 'file2.handler',
},
};

expect(() => extractFunctionEntries(cwd, 'azure', functionDefinitions)).toThrowError();
expect(consoleSpy).toBeCalled();
});
});
});

describe('getDepsFromBundle', () => {
Expand Down

0 comments on commit 47569f5

Please sign in to comment.