Skip to content

Commit

Permalink
feat(azure): support custom configuration (#1344)
Browse files Browse the repository at this point in the history
Co-authored-by: Pooya Parsa <pooya@pi0.io>
  • Loading branch information
NicholasDawson and pi0 authored Aug 20, 2023
1 parent 6cfdf01 commit 4a6cbcb
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 29 deletions.
19 changes: 19 additions & 0 deletions docs/content/2.deploy/providers/azure.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ NITRO_PRESET=azure yarn build
npx @azure/static-web-apps-cli start .output/public --api-location .output/server
```

### Configuration

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 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`. | `[]` |

### Custom Configuration

You can alter the Nitro generated configuration using `azure.config` option.

Custom routes will be added and matched first. In the case of a conflict (determined if an object has the same route property), custom routes will override generated ones.

### Deploy from CI/CD via GitHub Actions

When you link your GitHub repository to Azure Static Web Apps, a workflow file is added to the repository.
Expand Down
28 changes: 27 additions & 1 deletion src/presets/azure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,17 @@ async function writeRoutes(nitro: Nitro) {
}
}

// Merge custom config into the generated config
const config = {
...nitro.options.azure?.config,
routes: [], // Overwrite routes for now, we will add existing routes after generating routes
platform: {
apiRuntime: `node:${nodeVersion}`,
...nitro.options.azure?.config?.platform,
},
routes: [],
navigationFallback: {
rewrite: "/api/server",
...nitro.options.azure?.config?.navigationFallback,
},
};

Expand Down Expand Up @@ -99,6 +103,28 @@ async function writeRoutes(nitro: Nitro) {
});
}

// Prepend custom routes to the beginning of the routes array and override if they exist
if (
nitro.options.azure?.config &&
"routes" in nitro.options.azure.config &&
Array.isArray(nitro.options.azure.config.routes)
) {
// We iterate through the reverse so the order in the custom config is persisted
for (const customRoute of nitro.options.azure.config.routes.reverse()) {
const existingRouteMatchIndex = config.routes.findIndex(
(value) => value.route === customRoute.route
);

if (existingRouteMatchIndex === -1) {
// If we don't find a match, put the customRoute at the beginning of the array
config.routes.unshift(customRoute);
} else {
// Otherwise override the existing route with our customRoute
config.routes[existingRouteMatchIndex] = customRoute;
}
}
}

const functionDefinition = {
entryPoint: "handle",
bindings: [
Expand Down
15 changes: 15 additions & 0 deletions src/types/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,20 @@ export interface VercelServerlessFunctionConfig {
[key: string]: unknown;
}

interface AzureOptions {
config?: {
platform?: {
apiRuntime?: string;
[key: string]: unknown;
};
navigationFallback?: {
rewrite?: string;
[key: string]: unknown;
};
[key: string]: unknown;
};
}

export interface PresetOptions {
vercel: {
config: VercelBuildConfigV3;
Expand Down Expand Up @@ -166,4 +180,5 @@ export interface PresetOptions {
defaultRoutes?: boolean;
};
};
azure: AzureOptions;
}
109 changes: 109 additions & 0 deletions test/presets/azure.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { promises as fsp } from "node:fs";
import { resolve } from "pathe";
import { describe, it, expect } from "vitest";
import { fixtureDir, setupTest } from "../tests";

describe(
"nitro:preset:azure",
async () => {
const customConfig = {
routes: [
{
route: "/admin",
allowedRoles: ["authenticated"],
},
{
route: "/logout",
redirect: "/.auth/logout",
},
{
route: "/index.html",
redirect: "/overridden-index",
},
{
route: "/",
rewrite: "/api/server/overridden",
},
],
responseOverrides: {
401: {
statusCode: 302,
redirect: "/.auth/login/aad",
},
},
networking: {
allowedIpRanges: ["10.0.0.0/24", "100.0.0.0/32", "192.168.100.0/22"],
},
platform: {
apiRuntime: "custom-runtime",
},
};

const ctx = await setupTest("azure", {
config: {
azure: {
config: customConfig,
},
},
});

const config = await fsp
.readFile(resolve(ctx.rootDir, "staticwebapp.config.json"), "utf8")
.then((r) => JSON.parse(r));

it("generated the correct config", () => {
expect(config).toMatchInlineSnapshot(`
{
"navigationFallback": {
"rewrite": "/api/server",
},
"networking": {
"allowedIpRanges": [
"10.0.0.0/24",
"100.0.0.0/32",
"192.168.100.0/22",
],
},
"platform": {
"apiRuntime": "custom-runtime",
},
"responseOverrides": {
"401": {
"redirect": "/.auth/login/aad",
"statusCode": 302,
},
},
"routes": [
{
"allowedRoles": [
"authenticated",
],
"route": "/admin",
},
{
"redirect": "/.auth/logout",
"route": "/logout",
},
{
"rewrite": "/api/hey/index.html",
"route": "/api/hey",
},
{
"rewrite": "/prerender/index.html",
"route": "/prerender",
},
{
"redirect": "/overridden-index",
"route": "/index.html",
},
{
"rewrite": "/api/server/overridden",
"route": "/",
},
],
}
`);
});
},
{ timeout: 10_000 }
);
64 changes: 36 additions & 28 deletions test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { tmpdir } from "node:os";
import { promises as fsp } from "node:fs";
import { join, resolve } from "pathe";
import { listen, Listener } from "listhen";
import fse from "fs-extra";
import destr from "destr";
import { fetch, FetchOptions } from "ofetch";
import { expect, it, afterAll, beforeAll, describe } from "vitest";
import { fileURLToPath } from "mlly";
import { joinURL } from "ufo";
import { defu } from "defu";
import * as _nitro from "../src";
import type { Nitro } from "../src";

Expand Down Expand Up @@ -37,8 +37,14 @@ export const describeIf = (condition, title, factory) =>
it.skip("skipped", () => {});
});

export async function setupTest(preset: string) {
const fixtureDir = fileURLToPath(new URL("fixture", import.meta.url).href);
export const fixtureDir = fileURLToPath(
new URL("fixture", import.meta.url).href
);

export async function setupTest(
preset: string,
opts: { config?: _nitro.NitroConfig } = {}
) {
const presetTempDir = resolve(
process.env.NITRO_TEST_TMP_DIR || join(tmpdir(), "nitro-tests"),
preset
Expand Down Expand Up @@ -69,32 +75,34 @@ export async function setupTest(preset: string) {
process.env[name] = value;
}

const nitro = (ctx.nitro = await createNitro({
preset: ctx.preset,
dev: ctx.isDev,
rootDir: ctx.rootDir,
runtimeConfig: {
nitro: {
envPrefix: "CUSTOM_",
const nitro = (ctx.nitro = await createNitro(
defu(opts.config, {
preset: ctx.preset,
dev: ctx.isDev,
rootDir: ctx.rootDir,
runtimeConfig: {
nitro: {
envPrefix: "CUSTOM_",
},
hello: "",
helloThere: "",
},
hello: "",
helloThere: "",
},
buildDir: resolve(fixtureDir, presetTempDir, ".nitro"),
serveStatic:
preset !== "cloudflare" &&
preset !== "cloudflare-module" &&
preset !== "cloudflare-pages" &&
preset !== "vercel-edge" &&
!ctx.isDev,
output: {
dir: ctx.outDir,
},
timing:
preset !== "cloudflare" &&
preset !== "cloudflare-pages" &&
preset !== "vercel-edge",
}));
buildDir: resolve(fixtureDir, presetTempDir, ".nitro"),
serveStatic:
preset !== "cloudflare" &&
preset !== "cloudflare-module" &&
preset !== "cloudflare-pages" &&
preset !== "vercel-edge" &&
!ctx.isDev,
output: {
dir: ctx.outDir,
},
timing:
preset !== "cloudflare" &&
preset !== "cloudflare-pages" &&
preset !== "vercel-edge",
})
));

if (ctx.isDev) {
// Setup development server
Expand Down

0 comments on commit 4a6cbcb

Please sign in to comment.