Repository: geoffrich/svelte-adapter-azure-swa (js)

Issue:
Document API route limitation
See #78. If you have SvelteKit routes in `src/routes/api`, they will return a 404 when deployed to Azure. This is because Azure SWA reserves the `/api` path for individual Azure functions.

We could also issue some sort of warning if/when we detect these routes.

Pull request:
feat: throw error if /api routes defined
Closes #89

SWA reserves the `/api` prefix, so any overlapping SvelteKit routes will not be reachable in production and hitting them returns confusing errors (see linked issue and #78 for context).

This PR throws an error if it finds routes beginning with `/api` while building and instructions the user to rename the routes. If you do want to allow these routes for some reason, you can set `allowReservedSwaRoutes` in your adapter options. However, this will _not_ make it so SvelteKit /api routes are accessible in production, since this is an SWA limitation.

Note: this check will only run in SvelteKit 1.5 and later, since it uses a property added in that release.

Patch:
diff --git a/README.md b/README.md
index 418ec6c..306acdc 100644
--- a/README.md
+++ b/README.md
@@ -172,6 +172,25 @@ export default {
 };
 ```
 
+### allowReservedSwaRoutes
+
+In production, Azure SWA will route any requests to `/api` or `/api/*` to the SWA [API backend](https://learn.microsoft.com/en-us/azure/static-web-apps/apis-overview). If you also define SvelteKit routes beginning with `/api`, those requests will work in dev, but return a 404 in production since the request will be routed to the SWA API. Because of this, the adapter will throw an error at build time if it detects any routes beginning with `/api`.
+
+If you want to disable this check, you can set `allowReservedSwaRoutes` to true. However, this will not start routing `/api` requests to your SvelteKit app. SWA does not allow configuring the `/api` route.
+
+```js
+import azure from 'svelte-adapter-azure-swa';
+
+export default {
+	kit: {
+		...
+		adapter: azure({
+			allowReservedSwaRoutes: true
+		})
+	}
+};
+```
+
 ### esbuildOptions
 
 An object containing additional [esbuild options](https://esbuild.github.io/api/#build-api). Currently only supports [external](https://esbuild.github.io/api/#external). If you require additional options to be exposed, plese [open an issue](https://github.com/geoffrich/svelte-adapter-azure-swa/issues).
diff --git a/index.d.ts b/index.d.ts
index 5d793a5..ee46ac4 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -11,6 +11,7 @@ export type Options = {
 	esbuildOptions?: Pick<esbuild.BuildOptions, 'external'>;
 	apiDir?: string;
 	staticDir?: string;
+	allowReservedSwaRoutes?: boolean;
 };
 
 export default function plugin(options?: Options): Adapter;
diff --git a/index.js b/index.js
index bd5cb6d..45c6969 100644
--- a/index.js
+++ b/index.js
@@ -49,7 +49,8 @@ export default function ({
 	customStaticWebAppConfig = {},
 	esbuildOptions = {},
 	apiDir: customApiDir = undefined,
-	staticDir: customStaticDir = undefined
+	staticDir: customStaticDir = undefined,
+	allowReservedSwaRoutes = false
 } = {}) {
 	return {
 		name: 'adapter-azure-swa',
@@ -62,6 +63,24 @@ export default function ({
 Please see the PR for migration instructions: https://github.com/geoffrich/svelte-adapter-azure-swa/pull/92`
 				);
 			}
+
+			const conflictingRoutes =
+				builder.routes?.map((route) => route.id).filter((routeId) => routeId.startsWith('/api')) ??
+				[];
+			if (!allowReservedSwaRoutes && conflictingRoutes.length > 0) {
+				builder.log.error(
+					`Error: the following routes conflict with Azure SWA's reserved /api route: ${conflictingRoutes.join(
+						', '
+					)}. Requests to these routes in production will return 404 instead of hitting your SvelteKit app.
+
+To resolve this error, move the conflicting routes so they do not start with /api. For example, move /api/blog to /blog.
+If you want to suppress this error, set allowReservedSwaRoutes to true in your adapter options.
+					`
+				);
+
+				throw new Error('Conflicting routes detected. Please rename the routes listed above.');
+			}
+
 			const swaConfig = generateConfig(customStaticWebAppConfig, builder.config.kit.appDir);
 
 			const tmp = builder.getBuildDirectory('azure-tmp');


Test patch:
diff --git a/test/index.test.js b/test/index.test.js
index d90a37b..8009c74 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -123,6 +123,34 @@ describe('adapt', () => {
 			)
 		);
 	});
+
+	test.each(['/api', '/api/foo'])('throws error when the app defines %s route', async (routeId) => {
+		const adapter = azureAdapter();
+		const builder = getMockBuilder();
+		builder.routes.push({
+			id: routeId
+		});
+		await expect(adapter.adapt(builder)).rejects.toThrowError(
+			'Conflicting routes detected. Please rename the routes listed above.'
+		);
+	});
+
+	test('does not throw error for /api route if allowReservedSwaRoutes is defined', async () => {
+		const adapter = azureAdapter({ allowReservedSwaRoutes: true });
+		const builder = getMockBuilder();
+		builder.routes.push({
+			id: '/api'
+		});
+		await expect(adapter.adapt(builder)).resolves.not.toThrow();
+	});
+
+	test('handles null routes', async () => {
+		// builder.routes was added in 1.5 with route-level config
+		const adapter = azureAdapter();
+		const builder = getMockBuilder();
+		builder.routes = null;
+		await expect(adapter.adapt(builder)).resolves.not.toThrow();
+	});
 });
 
 /** @returns {import('@sveltejs/kit').Builder} */
@@ -135,7 +163,8 @@ function getMockBuilder() {
 		},
 		log: {
 			minor: vi.fn(),
-			warn: vi.fn()
+			warn: vi.fn(),
+			error: vi.fn()
 		},
 		prerendered: {
 			paths: ['/']
@@ -146,6 +175,14 @@ function getMockBuilder() {
 		getServerDirectory: vi.fn(() => 'server'),
 		rimraf: vi.fn(),
 		writeClient: vi.fn(),
-		writePrerendered: vi.fn()
+		writePrerendered: vi.fn(),
+		routes: [
+			{
+				id: '/'
+			},
+			{
+				id: '/about'
+			}
+		]
 	};
 }