diff --git a/docs/2.deploy/20.providers/azure.md b/docs/2.deploy/20.providers/azure.md index 9a465a694e..568e3fa4f3 100644 --- a/docs/2.deploy/20.providers/azure.md +++ b/docs/2.deploy/20.providers/azure.md @@ -12,7 +12,7 @@ Integration with this provider is possible with [zero configuration](/deploy/#zero-config-providers). :: -[Azure Static Web Apps](https://azure.microsoft.com/en-us/products/app-service/static) are designed to be deployed continuously in a [GitHub Actions workflow](https://docs.microsoft.com/en-us/azure/static-web-apps/github-actions-workflow). By default, Nitro will detect this deployment environment and enable the `azure` preset. +[Azure Static Web Apps](https://azure.microsoft.com/en-us/products/app-service/static) are designed to be deployed continuously in a [GitHub Actions workflow](https://docs.microsoft.com/en-us/azure/static-web-apps/github-actions-workflow). By default, Nitro will detect this deployment environment and enable the `azure-swa` preset. ### Local preview @@ -21,7 +21,7 @@ Install [Azure Functions Core Tools](https://docs.microsoft.com/en-us/azure/azur You can invoke a development environment to preview before deploying. ```bash -NITRO_PRESET=azure npx nypm@latest build +NITRO_PRESET=azure-swa npx nypm@latest build npx @azure/static-web-apps-cli start .output/public --api-location .output/server ``` @@ -29,14 +29,17 @@ npx @azure/static-web-apps-cli start .output/public --api-location .output/serve Azure Static Web Apps are [configured](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration) using the `staticwebapp.config.json` file. -Nitro automatically generates this configuration file whenever the application is built with the `azure` preset. +Nitro automatically generates this configuration file whenever the application is built with the `azure-swa` preset. Nitro will automatically add the following properties based on the following criteria: -| Property | Criteria | Default | -| --- | --- | --- | -| **[platform.apiRuntime](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#platform)** | Will automatically set to `node:16` or `node:14` depending on your package configuration. | `node:16` | -| **[navigationFallback.rewrite](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#fallback-routes)** | Is always `/api/server` | `/api/server` | -| **[routes](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#routes)** | All prerendered routes are added. Additionally, if you do not have an `index.html` file an empty one is created for you for compatibility purposes and also requests to `/index.html` are redirected to the root directory which is handled by `/api/server`. | `[]` | + + +| Property | Criteria | Default | +| ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| **[platform.apiRuntime](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#platform)** | Uses `node:20` or `node:22` when `package.json` `engines.node` (or your current Node major version) matches those supported versions; otherwise falls back to `node:18`. | `node:18` | +| **[navigationFallback.rewrite](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#fallback-routes)** | Is always `/api/server` | `/api/server` | +| **[routes](https://learn.microsoft.com/en-us/azure/static-web-apps/configuration#routes)** | All prerendered routes are added. Additionally, if you do not have an `index.html` file an empty one is created for you for compatibility purposes and also requests to `/index.html` are redirected to the root directory which is handled by `/api/server`. | `[]` | + ### Custom configuration @@ -50,12 +53,14 @@ When you link your GitHub repository to Azure Static Web Apps, a workflow file i When you are asked to select your framework, select custom and provide the following information: -| Input | Value | -| --- | --- | -| **app_location** | '/' | -| **api_location** | '.output/server' | + +| Input | Value | +| ------------------- | ---------------- | +| **app_location** | '/' | +| **api_location** | '.output/server' | | **output_location** | '.output/public' | + If you miss this step, you can always find the build configuration section in your workflow and update the build configuration: ```yaml [.github/workflows/azure-static-web-apps-.yml] @@ -68,5 +73,4 @@ output_location: '.output/public' That's it! Now Azure Static Web Apps will automatically deploy your Nitro-powered application on push. -If you are using runtimeConfig, you will likely want to configure the corresponding [environment variables on Azure](https://docs.microsoft.com/en-us/azure/static-web-apps/application-settings). - +If you are using runtimeConfig, you will likely want to configure the corresponding [environment variables on Azure](https://docs.microsoft.com/en-us/azure/static-web-apps/application-settings). \ No newline at end of file diff --git a/src/presets/azure/runtime/_utils.ts b/src/presets/azure/runtime/_utils.ts index 919e959c56..4551c89167 100644 --- a/src/presets/azure/runtime/_utils.ts +++ b/src/presets/azure/runtime/_utils.ts @@ -1,4 +1,4 @@ -import type { Cookie } from "@azure/functions"; +import type { Cookie, HttpRequest } from "@azure/functions"; import { parse } from "cookie-es"; export function getAzureParsedCookiesFromHeaders(headers: Headers): Cookie[] { @@ -32,6 +32,32 @@ export function getAzureParsedCookiesFromHeaders(headers: Headers): Cookie[] { return azureCookies; } +export function resolveBaseUrl(req: HttpRequest) { + const forwardedProto = req.headers["x-forwarded-proto"]; + const forwardedHost = req.headers["x-forwarded-host"]; + const host = forwardedHost || req.headers["host"]; + if (host) { + const candidate = `${forwardedProto || "http"}://${host}`; + try { + return new URL(candidate).origin; + } catch (error) { + console.warn("[nitro] Invalid Azure SWA forwarded origin", { + candidate, + error, + }); + } + } + const originalUrl = req.headers["x-ms-original-url"]; + if (originalUrl) { + try { + return new URL(originalUrl).origin; + } catch { + // ignore invalid original URL + } + } + return "http://localhost"; +} + function parseNumberOrDate(expires: string) { const expiresAsNumber = parseNumber(expires); if (expiresAsNumber !== undefined) { diff --git a/src/presets/azure/runtime/azure-swa.ts b/src/presets/azure/runtime/azure-swa.ts index de7cf65860..9d4e178da1 100644 --- a/src/presets/azure/runtime/azure-swa.ts +++ b/src/presets/azure/runtime/azure-swa.ts @@ -1,7 +1,7 @@ import "#nitro/virtual/polyfills"; import { parseURL } from "ufo"; import { useNitroApp } from "nitro/app"; -import { getAzureParsedCookiesFromHeaders } from "./_utils.ts"; +import { getAzureParsedCookiesFromHeaders, resolveBaseUrl } from "./_utils.ts"; import type { HttpRequest, HttpResponse, HttpResponseSimple } from "@azure/functions"; @@ -19,8 +19,9 @@ export async function handle(context: { res: HttpResponse }, req: HttpRequest) { url = "/api/" + (req.params.url || ""); } - const request = new Request(url, { + const request = new Request(new URL(url, resolveBaseUrl(req)), { method: req.method || undefined, + headers: new Headers(req.headers), // https://github.com/Azure/azure-functions-nodejs-worker/issues/294 // https://github.com/Azure/azure-functions-host/issues/293 body: req.bufferBody ?? req.rawBody, @@ -32,7 +33,7 @@ export async function handle(context: { res: HttpResponse }, req: HttpRequest) { // (v4) https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-node?tabs=typescript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#http-response context.res = { status: response.status, - body: response.body, + body: response.body ? Buffer.from(await response.arrayBuffer()) : undefined, cookies: getAzureParsedCookiesFromHeaders(response.headers), headers: Object.fromEntries( [...response.headers.entries()].filter(([key]) => key !== "set-cookie")