Skip to content

Commit

Permalink
feat: switch to Azure Functions v4
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
derkoe committed Jun 23, 2024
1 parent 9b4d834 commit 2c03215
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 98 deletions.
2 changes: 1 addition & 1 deletion demo/func/host.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[2.*, 3.0.0)"
"version": "[4.0.0, 5.0.0)"
}
}
2 changes: 1 addition & 1 deletion files/api/host.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[2.*, 3.0.0)"
"version": "[4.0.0, 5.0.0)"
}
}
7 changes: 6 additions & 1 deletion files/api/package.json
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
{}
{
"main": "sk_render/index.js",
"dependencies": {
"@azure/functions": "^4"
}
}
120 changes: 60 additions & 60 deletions files/entry.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getClientPrincipalFromHeaders,
splitCookiesFromHeaders
} from './headers';
import { app } from '@azure/functions';

// replaced at build time
// @ts-expect-error
Expand All @@ -17,87 +18,86 @@ const server = new Server(manifest);
const initialized = server.init({ env: process.env });

/**
* @typedef {import('@azure/functions').AzureFunction} AzureFunction
* @typedef {import('@azure/functions').Context} Context
* @typedef {import('@azure/functions').InvocationContext} InvocationContext
* @typedef {import('@azure/functions').HttpRequest} HttpRequest
* @typedef {import('@azure/functions').HttpResponse} HttpResponse
*/

/**
* @param {Context} context
*/
export async function index(context) {
const request = toRequest(context);

if (debug) {
context.log(`Request: ${JSON.stringify(request)}`);
}

const ipAddress = getClientIPFromHeaders(request.headers);
const clientPrincipal = getClientPrincipalFromHeaders(request.headers);

await initialized;
const rendered = await server.respond(request, {
getClientAddress() {
return ipAddress;
},
platform: {
clientPrincipal,
context
app.http('sk_render', {
methods: ['HEAD', 'GET', 'POST', 'DELETE', 'PUT', 'OPTIONS'],
/**
*
* @param {HttpRequest} httpRequest
* @param {InvocationContext} context
*/
handler: async (httpRequest, context) => {
if (debug) {
context.log(`Request: ${JSON.stringify(httpRequest)}`);
}
});

const response = await toResponse(rendered);
const request = toRequest(httpRequest);

const ipAddress = getClientIPFromHeaders(request.headers);
const clientPrincipal = getClientPrincipalFromHeaders(request.headers);

await initialized;
const rendered = await server.respond(request, {
getClientAddress() {
return ipAddress;
},
platform: {
user: httpRequest.user,
clientPrincipal,
context
}
});

if (debug) {
context.log(`SK headers: ${JSON.stringify(Object.fromEntries(rendered.headers.entries()))}`);
context.log(`Response: ${JSON.stringify(rendered)}`);
}

if (debug) {
context.log(`SK headers: ${JSON.stringify(Object.fromEntries(rendered.headers.entries()))}`);
context.log(`Response: ${JSON.stringify(response)}`);
return toResponse(rendered);
}

context.res = response;
}
});

/**
* @param {Context} context
* @param {HttpRequest} httpRequest
* @returns {Request}
* */
function toRequest(context) {
const { method, headers, rawBody, body } = context.req;
// because we proxy all requests to the render function, the original URL in the request is /api/__render
// this header contains the URL the user requested
const originalUrl = headers['x-ms-original-url'];

/** @type {RequestInit} */
const init = {
method,
headers: new Headers(headers)
};

if (method !== 'GET' && method !== 'HEAD') {
init.body = Buffer.isBuffer(body)
? body
: typeof rawBody === 'string'
? Buffer.from(rawBody, 'utf-8')
: rawBody;
}
*/
function toRequest(httpRequest) {
const originalUrl = httpRequest.headers.get('x-ms-original-url');

/** @type {Record<string, string>} */
const headers = {};
httpRequest.headers.forEach((value, key) => {
if (key !== 'x-ms-original-url') {
headers[key] = value;
}
});

return new Request(originalUrl, init);
return new Request(originalUrl, {
method: httpRequest.method,
headers: new Headers(headers),
// @ts-ignore
body: httpRequest.body,
duplex: 'half'
});
}

/**
* @param {Response} rendered
* @returns {Promise<Record<string, any>>}
* @returns {Promise<HttpResponse>}
*/
async function toResponse(rendered) {
const { status } = rendered;
const resBody = new Uint8Array(await rendered.arrayBuffer());

const { headers, cookies } = splitCookiesFromHeaders(rendered.headers);

return {
status,
body: resBody,
status: rendered.status,
// @ts-ignore
body: rendered.body,
headers,
cookies,
isRaw: true
enableContentNegotiation: false
};
}
13 changes: 9 additions & 4 deletions files/headers.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
import * as set_cookie_parser from 'set-cookie-parser';

/**
* @typedef {import('@azure/functions').Cookie} Cookie
*/

/**
* Splits 'set-cookie' headers into individual cookies
* @param {Headers} headers
* @returns {{
* headers: Record<string, string>,
* cookies: set_cookie_parser.Cookie[]
* headers: Headers,
* cookies: Cookie[]
* }}
*/
export function splitCookiesFromHeaders(headers) {
/** @type {Record<string, string>} */
const resHeaders = {};

/** @type {set_cookie_parser.Cookie[]} */
/** @type {Cookie[]} */
const resCookies = [];

headers.forEach((value, key) => {
if (key === 'set-cookie') {
const cookieStrings = set_cookie_parser.splitCookiesString(value);
// @ts-ignore
resCookies.push(...set_cookie_parser.parse(cookieStrings));
} else {
resHeaders[key] = value;
}
});

return { headers: resHeaders, cookies: resCookies };
return { headers: new Headers(resHeaders), cookies: resCookies };
}

/**
Expand Down
7 changes: 5 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Adapter } from '@sveltejs/kit';
import { ClientPrincipal, CustomStaticWebAppConfig } from './types/swa';
import { Context } from '@azure/functions';
import { HttpRequestUser, InvocationContext } from '@azure/functions';
import esbuild from 'esbuild';

export * from './types/swa';
Expand Down Expand Up @@ -37,8 +37,11 @@ declare global {
*
* @see The {@link https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node#context-object Azure function documentation}
*/
context: InvocationContext;

user: HttpRequestUser;

clientPrincipal?: ClientPrincipal;
context: Context;
}
}
}
22 changes: 1 addition & 21 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,7 @@ import esbuild from 'esbuild';
* @typedef {import('esbuild').BuildOptions} BuildOptions
*/

const ssrFunctionRoute = '/api/__render';

const functionJson = `
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"route": "__render"
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}
`;
const ssrFunctionRoute = '/api/sk_render';

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

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

builder.log.minor('Copying assets...');
builder.writeClient(staticDir);
Expand Down
49 changes: 42 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@sveltejs/kit": "^2.0.0"
},
"devDependencies": {
"@azure/functions": "^1.2.3",
"@azure/functions": "^4",
"@sveltejs/kit": "^2.0.4",
"@types/node": "^18.19.3",
"@types/set-cookie-parser": "^2.4.7",
Expand Down

0 comments on commit 2c03215

Please sign in to comment.