Skip to content

Commit c7da645

Browse files
committed
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 geoffrich#159
1 parent d454147 commit c7da645

File tree

9 files changed

+126
-110
lines changed

9 files changed

+126
-110
lines changed

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: 60 additions & 72 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,86 @@ 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(`Request: ${JSON.stringify(httpRequest)}`);
5236
}
53-
});
5437

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

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

6564
/**
66-
* @param {Context} context
65+
* @param {HttpRequest} httpRequest
6766
* @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-
// this header contains the URL the user requested
73-
const originalUrl = headers['x-ms-original-url'];
74-
75-
// SWA strips content-type headers from empty POST requests, but SK form actions require the header
76-
// 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';
79-
}
80-
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-
}
67+
*/
68+
function toRequest(httpRequest) {
69+
const originalUrl = httpRequest.headers.get('x-ms-original-url');
70+
71+
/** @type {Record<string, string>} */
72+
const headers = {};
73+
httpRequest.headers.forEach((value, key) => {
74+
if (key !== 'x-ms-original-url') {
75+
headers[key] = value;
76+
}
77+
});
9478

95-
return new Request(originalUrl, init);
79+
return new Request(originalUrl, {
80+
method: httpRequest.method,
81+
headers: new Headers(headers),
82+
// @ts-ignore
83+
body: httpRequest.body,
84+
duplex: 'half'
85+
});
9686
}
9787

9888
/**
9989
* @param {Response} rendered
100-
* @returns {Promise<Record<string, any>>}
90+
* @returns {Promise<HttpResponse>}
10191
*/
10292
async function toResponse(rendered) {
103-
const { status } = rendered;
104-
const resBody = new Uint8Array(await rendered.arrayBuffer());
105-
10693
const { headers, cookies } = splitCookiesFromHeaders(rendered.headers);
10794

10895
return {
109-
status,
110-
body: resBody,
96+
status: rendered.status,
97+
// @ts-ignore
98+
body: rendered.body,
11199
headers,
112100
cookies,
113-
isRaw: true
101+
enableContentNegotiation: false
114102
};
115103
}

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-ignore
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
}

index.js

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,7 @@ import esbuild from 'esbuild';
77
* @typedef {import('esbuild').BuildOptions} BuildOptions
88
*/
99

10-
const ssrFunctionRoute = '/api/__render';
11-
12-
const functionJson = `
13-
{
14-
"bindings": [
15-
{
16-
"authLevel": "anonymous",
17-
"type": "httpTrigger",
18-
"direction": "in",
19-
"name": "req",
20-
"route": "__render"
21-
},
22-
{
23-
"type": "http",
24-
"direction": "out",
25-
"name": "res"
26-
}
27-
]
28-
}
29-
`;
10+
const ssrFunctionRoute = '/api/sk_render';
3011

3112
/**
3213
* Validate the static web app configuration does not override the minimum config for the adapter to work correctly.
@@ -137,7 +118,6 @@ If you want to suppress this error, set allowReservedSwaRoutes to true in your a
137118
};
138119

139120
await esbuild.build(default_options);
140-
writeFileSync(join(functionDir, 'function.json'), functionJson);
141121

142122
builder.log.minor('Copying assets...');
143123
builder.writeClient(staticDir);

package-lock.json

Lines changed: 42 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"@sveltejs/kit": "^2.0.0"
3535
},
3636
"devDependencies": {
37-
"@azure/functions": "^1.2.3",
37+
"@azure/functions": "^4",
3838
"@sveltejs/kit": "^2.0.4",
3939
"@types/node": "^18.19.3",
4040
"@types/set-cookie-parser": "^2.4.7",

0 commit comments

Comments
 (0)