Skip to content

Commit 96e6067

Browse files
derkoegeoffrich
andauthored
feat: switch to Azure Functions v4 (#177)
* feat: switch to Azure Functions v4 * remove function.json - it is not needed anymore * rename function path to "sk_render" since v4 does not allow the path to start with "__" See also: https://techcommunity.microsoft.com/t5/apps-on-azure-blog/azure-functions-version-4-of-the-node-js-programming-model-is-in/ba-p/3773541 closes #159 * add @azure/functions to external esbuild options (if not already there) * Port over changes from #179 * Fix one unit test and update docs / release notes * fix test 'writes to custom api directory' * convert external function to v4 * fix headers test - use fetch API * Update comments and docs --------- Co-authored-by: Geoff Rich <[email protected]>
1 parent d454147 commit 96e6067

File tree

15 files changed

+189
-146
lines changed

15 files changed

+189
-146
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
### Features
6+
7+
* **Breaking:** switch to Azure Functions v4:
8+
* the function path has changed from `/api/__render` to `/api/sk_render` (v4 does not allow routes starting with underscores)
9+
* see also [Migrate to version 4 of the Node.js programming model for Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-node-upgrade-v4) from the Azure docs
10+
11+
12+
313
### [0.20.1](https://www.github.com/geoffrich/svelte-adapter-azure-swa/compare/v0.20.0...v0.20.1) (2024-07-13)
414

515

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ custom/
119119
└── index.js
120120
```
121121

122-
Also note that the adapter reserves the folder prefix `sk_render` and API route prefix `__render` for Azure functions generated by the adapter. So, if you use a custom API directory, you cannot have any other folder starting with `sk_render` or functions available at the `__render` route, since these will conflict with the adapter's Azure functions.
122+
Also note that the adapter reserves the folder prefix `sk_render` and API route prefix `sk_render` for Azure functions generated by the adapter. So, if you use a custom API directory, you cannot have any other folder starting with `sk_render` or functions available at the `sk_render` route, since these will conflict with the adapter's Azure functions.
123123

124124
### staticDir
125125

@@ -240,6 +240,10 @@ This is currently only available when running in production on SWA. In addition,
240240

241241
All server requests to your SvelteKit app are handled by an Azure function. This property contains that Azure function's [request context](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node#context-object).
242242

243+
### `user`
244+
245+
The `user` property of the Azure function's [HTTP request](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=javascript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#http-request).
246+
243247
## Monorepo support
244248

245249
If you're deploying your app from a monorepo, here's what you need to know.

demo/func/HelloWorld/function.json

Lines changed: 0 additions & 16 deletions
This file was deleted.

demo/func/HelloWorld/index.js

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
1-
module.exports = async function (context, req) {
2-
context.log('JavaScript HTTP trigger function processed a request.');
3-
4-
const name = req.query.name || (req.body && req.body.name);
5-
const responseMessage = name
6-
? 'Hello, ' + name + '. This HTTP triggered function executed successfully.'
7-
: 'This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.';
8-
9-
context.res = {
10-
// status: 200, /* Defaults to 200 */
11-
body: responseMessage
12-
};
13-
};
1+
const { app } = require('@azure/functions');
2+
3+
app.http('httpTrigger1', {
4+
methods: ['GET', 'POST'],
5+
handler: async (req, context) => {
6+
context.log('JavaScript HTTP trigger function processed a request.');
7+
8+
let name;
9+
if (req.query.has('name')) {
10+
name = req.query.get('name')
11+
} else {
12+
let body = await req.json();
13+
name = body.name;
14+
}
15+
16+
const responseMessage = name
17+
? 'Hello, ' + name + '. This HTTP triggered function executed successfully.'
18+
: 'This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.';
19+
20+
return { body: responseMessage };
21+
}
22+
});

demo/func/host.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
"version": "2.0",
33
"extensionBundle": {
44
"id": "Microsoft.Azure.Functions.ExtensionBundle",
5-
"version": "[2.*, 3.0.0)"
5+
"version": "[4.0.0, 5.0.0)"
66
}
77
}

files/api/host.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22
"version": "2.0",
33
"extensionBundle": {
44
"id": "Microsoft.Azure.Functions.ExtensionBundle",
5-
"version": "[2.*, 3.0.0)"
5+
"version": "[4.0.0, 5.0.0)"
66
}
77
}

files/api/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
{}
1+
{
2+
"main": "sk_render/index.js",
3+
"dependencies": {
4+
"@azure/functions": "^4"
5+
}
6+
}

files/entry.js

Lines changed: 71 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getClientPrincipalFromHeaders,
77
splitCookiesFromHeaders
88
} from './headers';
9+
import { app } from '@azure/functions';
910

1011
// replaced at build time
1112
// @ts-expect-error
@@ -17,99 +18,103 @@ const server = new Server(manifest);
1718
const initialized = server.init({ env: process.env });
1819

1920
/**
20-
* @typedef {import('@azure/functions').AzureFunction} AzureFunction
21-
* @typedef {import('@azure/functions').Context} Context
21+
* @typedef {import('@azure/functions').InvocationContext} InvocationContext
2222
* @typedef {import('@azure/functions').HttpRequest} HttpRequest
23+
* @typedef {import('@azure/functions').HttpResponse} HttpResponse
2324
*/
2425

25-
/**
26-
* @param {Context} context
27-
*/
28-
export async function index(context) {
29-
const request = toRequest(context);
30-
31-
if (debug) {
32-
context.log(
33-
'Starting request',
34-
context?.req?.method,
35-
context?.req?.headers?.['x-ms-original-url']
36-
);
37-
context.log(`Original request: ${JSON.stringify(context)}`);
38-
context.log(`Request: ${JSON.stringify(request)}`);
39-
}
40-
41-
const ipAddress = getClientIPFromHeaders(request.headers);
42-
const clientPrincipal = getClientPrincipalFromHeaders(request.headers);
43-
44-
await initialized;
45-
const rendered = await server.respond(request, {
46-
getClientAddress() {
47-
return ipAddress;
48-
},
49-
platform: {
50-
clientPrincipal,
51-
context
26+
app.http('sk_render', {
27+
methods: ['HEAD', 'GET', 'POST', 'DELETE', 'PUT', 'OPTIONS'],
28+
/**
29+
*
30+
* @param {HttpRequest} httpRequest
31+
* @param {InvocationContext} context
32+
*/
33+
handler: async (httpRequest, context) => {
34+
if (debug) {
35+
context.log(
36+
'Starting request',
37+
httpRequest.method,
38+
httpRequest.headers.get('x-ms-original-url')
39+
);
40+
context.log(`Request: ${JSON.stringify(httpRequest)}`);
5241
}
53-
});
5442

55-
const response = await toResponse(rendered);
43+
const request = toRequest(httpRequest);
44+
45+
const ipAddress = getClientIPFromHeaders(request.headers);
46+
const clientPrincipal = getClientPrincipalFromHeaders(request.headers);
47+
48+
await initialized;
49+
const rendered = await server.respond(request, {
50+
getClientAddress() {
51+
return ipAddress;
52+
},
53+
platform: {
54+
user: httpRequest.user,
55+
clientPrincipal,
56+
context
57+
}
58+
});
59+
60+
if (debug) {
61+
context.log(`SK headers: ${JSON.stringify(Object.fromEntries(rendered.headers.entries()))}`);
62+
context.log(`Response: ${JSON.stringify(rendered)}`);
63+
}
5664

57-
if (debug) {
58-
context.log(`SK headers: ${JSON.stringify(Object.fromEntries(rendered.headers.entries()))}`);
59-
context.log(`Response: ${JSON.stringify(response)}`);
65+
return toResponse(rendered);
6066
}
61-
62-
context.res = response;
63-
}
67+
});
6468

6569
/**
66-
* @param {Context} context
70+
* @param {HttpRequest} httpRequest
6771
* @returns {Request}
68-
* */
69-
function toRequest(context) {
70-
const { method, headers, rawBody, body } = context.req;
71-
// because we proxy all requests to the render function, the original URL in the request is /api/__render
72+
*/
73+
function toRequest(httpRequest) {
74+
// because we proxy all requests to the render function, the original URL in the request is /api/sk_render
7275
// this header contains the URL the user requested
73-
const originalUrl = headers['x-ms-original-url'];
76+
const originalUrl = httpRequest.headers.get('x-ms-original-url');
7477

7578
// SWA strips content-type headers from empty POST requests, but SK form actions require the header
7679
// https://github.com/geoffrich/svelte-adapter-azure-swa/issues/178
77-
if (method === 'POST' && !body && !headers['content-type']) {
78-
headers['content-type'] = 'application/x-www-form-urlencoded';
80+
if (
81+
httpRequest.method === 'POST' &&
82+
!httpRequest.body &&
83+
!httpRequest.headers.get('content-type')
84+
) {
85+
httpRequest.headers.set('content-type', 'application/x-www-form-urlencoded');
7986
}
8087

81-
/** @type {RequestInit} */
82-
const init = {
83-
method,
84-
headers: new Headers(headers)
85-
};
86-
87-
if (method !== 'GET' && method !== 'HEAD') {
88-
init.body = Buffer.isBuffer(body)
89-
? body
90-
: typeof rawBody === 'string'
91-
? Buffer.from(rawBody, 'utf-8')
92-
: rawBody;
93-
}
88+
/** @type {Record<string, string>} */
89+
const headers = {};
90+
httpRequest.headers.forEach((value, key) => {
91+
if (key !== 'x-ms-original-url') {
92+
headers[key] = value;
93+
}
94+
});
9495

95-
return new Request(originalUrl, init);
96+
return new Request(originalUrl, {
97+
method: httpRequest.method,
98+
headers: new Headers(headers),
99+
// @ts-ignore
100+
body: httpRequest.body,
101+
duplex: 'half'
102+
});
96103
}
97104

98105
/**
99106
* @param {Response} rendered
100-
* @returns {Promise<Record<string, any>>}
107+
* @returns {Promise<HttpResponse>}
101108
*/
102109
async function toResponse(rendered) {
103-
const { status } = rendered;
104-
const resBody = new Uint8Array(await rendered.arrayBuffer());
105-
106110
const { headers, cookies } = splitCookiesFromHeaders(rendered.headers);
107111

108112
return {
109-
status,
110-
body: resBody,
113+
status: rendered.status,
114+
// @ts-ignore
115+
body: rendered.body,
111116
headers,
112117
cookies,
113-
isRaw: true
118+
enableContentNegotiation: false
114119
};
115120
}

files/headers.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,35 @@
11
import * as set_cookie_parser from 'set-cookie-parser';
22

3+
/**
4+
* @typedef {import('@azure/functions').Cookie} Cookie
5+
*/
6+
37
/**
48
* Splits 'set-cookie' headers into individual cookies
59
* @param {Headers} headers
610
* @returns {{
7-
* headers: Record<string, string>,
8-
* cookies: set_cookie_parser.Cookie[]
11+
* headers: Headers,
12+
* cookies: Cookie[]
913
* }}
1014
*/
1115
export function splitCookiesFromHeaders(headers) {
1216
/** @type {Record<string, string>} */
1317
const resHeaders = {};
1418

15-
/** @type {set_cookie_parser.Cookie[]} */
19+
/** @type {Cookie[]} */
1620
const resCookies = [];
1721

1822
headers.forEach((value, key) => {
1923
if (key === 'set-cookie') {
2024
const cookieStrings = set_cookie_parser.splitCookiesString(value);
25+
// @ts-expect-error - one cookie type has a stricter sameSite type
2126
resCookies.push(...set_cookie_parser.parse(cookieStrings));
2227
} else {
2328
resHeaders[key] = value;
2429
}
2530
});
2631

27-
return { headers: resHeaders, cookies: resCookies };
32+
return { headers: new Headers(resHeaders), cookies: resCookies };
2833
}
2934

3035
/**

index.d.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Adapter } from '@sveltejs/kit';
22
import { ClientPrincipal, CustomStaticWebAppConfig } from './types/swa';
3-
import { Context } from '@azure/functions';
3+
import { HttpRequestUser, InvocationContext } from '@azure/functions';
44
import esbuild from 'esbuild';
55

66
export * from './types/swa';
@@ -37,8 +37,11 @@ declare global {
3737
*
3838
* @see The {@link https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node#context-object Azure function documentation}
3939
*/
40+
context: InvocationContext;
41+
42+
user: HttpRequestUser;
43+
4044
clientPrincipal?: ClientPrincipal;
41-
context: Context;
4245
}
4346
}
4447
}

0 commit comments

Comments
 (0)