svelte-adapter-azure-swa
svelte-adapter-azure-swa copied to clipboard
Investigate improving SWA CLI story
Right now our docs say you need to run a full build before running locally with the Azure SWA CLI. This means you don't get live updates or HMR. We should see if there's a way to improve this and document accordingly. I would think there's something since Next/Nuxt are also supported.
Context from Twitter:
Do you know if it is possible to combine Vite dev server with the local swa runner?
Currently I'm using "swa start --run 'vite build --watch'" which builds the app on changes but you'll have to reload the app in the browser.
Part of this should also be updating the demo project to use whatever setup is discovered.
I have also tried to run:
swa start http://localhost:5173/ --api-location http://localhost:5173/ --run 'vite dev'
This works partially but throws errors on server-side rendering:
[run] Not found: /api/__render
[run] Error: Not found: /api/__render
[run] at resolve (file:///home/user/myapp/node_modules/@sveltejs/kit/src/runtime/server/index.js:259:13)
[run] at resolve (file:///home/user/myapp/node_modules/@sveltejs/kit/src/runtime/server/index.js:290:5)
[run] at Object.handle (file:///home/user/myapp/node_modules/@sveltejs/kit/src/exports/vite/dev/index.js:340:66)
[run] at respond (file:///home/user/myapp/node_modules/@sveltejs/kit/src/runtime/server/index.js:287:40)
[run] at processTicksAndRejections (node:internal/process/task_queues:96:5)
[run] at async file:///home/user/myapp/node_modules/@sveltejs/kit/src/exports/vite/dev/index.js:410:22
Hm, I wonder if there's a way to write a custom handle hook in SvelteKit that rewrites the request to /api/__render accordingly (docs)
I figured something out - if we point SWA CLI at the SvelteKit dev server for the Azure function API as well, we won't need to build on every change. However, then SvelteKit gets requests to "/api/__render" that it doesn't know what to do with. We can rewrite those requests using a Vite plugin that applies a middleware that parses the x-ms-original-url header (using similar logic to the adapter in production).
// vite.config.js
import { sveltekit } from "@sveltejs/kit/vite";
const swaPlugin = () => ({
name: "configure-swa-proxy",
/**
* @param {import('vite').ViteDevServer} server
*/
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === "/api/__render") {
const originalUrl = req.headers["x-ms-original-url"];
const parsedUrl = new URL(originalUrl);
const rewrittenUrl = parsedUrl.pathname + parsedUrl.search;
req.url = rewrittenUrl;
req.originalUrl = rewrittenUrl;
}
next();
});
}
});
/** @type {import('vite').UserConfig} */
const config = {
plugins: [swaPlugin(), sveltekit()]
};
export default config;
We can then use the following swa-cli.config.json to point at the SvelteKit dev server for all requests:
{
"configurations": {
"app": {
"apiLocation": "http://localhost:5173",
"appDevserverUrl": "http://localhost:5173",
"run": "npm run dev",
"swaConfigLocation": "./build"
}
}
}
and run swa start to start the SWA emulator on localhost:4280. You should then be able to browse the site on localhost:4280 and requests should be proxied appropriately. Any dev changes should be applied immediately, with one exception - if you update your customStaticWebAppConfig passed to the adapter, you will need to run npm run build to create a new staticwebapp.config.json in the build folder.
I'd appreciate it if anyone following the thread can try this out and see if it works for them.
TODOs for this package (assuming it works)
- [ ] export vite plugin that proxies requests, sharing logic with the actual adapter
- [ ] document solution in README
Thx - I have tried your setup. Unauthorized requests work without issues.
I have found an issue with authentication. After the login via /.auth/login/aad the first requests includes the x-ms-client-principal. When I go to another page or refresh the page the header is not there anymore. I have to dig a bit deeper to find out what's causing this.
Here is my sample: https://github.com/derkoe/bazaar-sveltekit
Seems to work in general for me, I didn't poke much around, though.
As I mentioned in #102, I would love for a way to run a production build through swa, including simulation of Azure functions. I needed this to debug a failure the other day, and was able to get it to work only because I didnt' need the frontend.
Running a production build seems to be only about finding the right configuration for swa, though I wasn't able to find it after looking for half an hour the other day.
As I mentioned in https://github.com/geoffrich/svelte-adapter-azure-swa/issues/102, I would love for a way to run a production build through swa, including simulation of Azure functions. I needed this to debug a failure the other day, and was able to get it to work only because I didnt' need the frontend.
I may be misunderstanding, but I think you can do that by running npm run build and using the current configuration in the README. It points swa at the build static files and the Azure function the adapter uses for SSR. You'll have to rebuild on each change, though.
{
"configurations": {
"app": {
"outputLocation": "./build/static",
"apiLocation": "./build/server"
}
}
}
Good callout, though, we should probably have information about both configurations in the README.
@derkoe looks like your repro is private, I get a 404. Though this sounds similar to other issues I ran into trying to access the x-ms-client-principal header, though I think I only ran into them when testing the deployed site, not with the SWA CLI. See https://github.com/geoffrich/svelte-adapter-azure-swa/issues/102#issuecomment-1399696432 for more details.
I may be misunderstanding, but I think you can do that by running
npm run buildand using the current configuration in the README. It pointsswaat the build static files and the Azure function the adapter uses for SSR. You'll have to rebuild on each change, though.
This doesn't work for me. I get the following logs:
[api]
[api] Azure Functions Core Tools
[api] Core Tools Version: 4.0.4915 Commit hash: N/A (64-bit)
[api] Function Runtime Version: 4.14.0.19631
[api]
[swa]
[swa] Found configuration file:
[swa] /home/tlaundal/Projects/S<snip>e/build/staticwebapp.config.json
[swa]
[swa] - Waiting for http://localhost:7071 to be ready
[api]
[api] Functions:
[api]
[api] sk_render: http://localhost:7071/api/__render
[api]
[api] For detailed output, run func with --verbose flag.
[api] [2023-01-24T17:34:14.260Z] Worker process started and initialized.
[api] [2023-01-24T17:34:14.550Z] Initializing services
[api] [2023-01-24T17:34:15.076Z] Services initialized.
[api] [2023-01-24T17:34:18.490Z] Host lock lease acquired by instance ID '0000000000000000000000002E0F1A26'.
[swa] ✖ Waiting for http://localhost:7071 to be ready
[swa] ✖ Could not connect to "http://localhost:7071". Is the server up and running?
[swa] node "/usr/local/lib/node_modules/@azure/static-web-apps-cli/dist/msha/server.js" exited with code 0
--> Sending SIGTERM to other processes..
The ✖ comes after quite a long wait. What's weird is that in the meantime I can navigate to http://localhost:7071 in my browser and get a page saying "Your Functions 4.0 app is up and running". The problem seems to be that swa doesn't pick up that the functions are ready.
I just solved the above issue, and it seems to be a bug in swa.
What happens is that swa tries to make connections to localhost:7071, while azure functions core tools starts listening on 127.0.0.1:7071. My system just so happens to resolve localhost to ::1, so swa is never able to connect.
It seems I can fix this by setting configurations.app.host to 127.0.0.1 in my swa-cli.config.json.
Will file an issue with swa for this.
@geoffrich sorry - made it public: https://github.com/derkoe/bazaar-sveltekit.
It's really strange why the header is not there on some requests. I have the same app running with Remix using @derkoe/remix-azure-functions and there the auth works with locally using swa and on Azure.
Here's another caveat to keep in mind about swa, it doesn't support pre-generated routes very well, as it hasn't implemented support for rewriting /my/route to /my/route.html. See Azure/static-web-apps-cli#418
It's really strange why the header is not there on some requests. I have the same app running with Remix using @derkoe/remix-azure-functions and there the auth works with locally using swa and on Azure.
Ran your demo repo, and huh, you're right. This is extra weird since I have a (vanilla) demo repo with a similar config setup where I do get the x-ms-client-principal header back in all cases when running locally. In any event, an inconsistent auth header is a problem.
I think the difference between this adapter and the remix one is that this adapter only overrides the * route for non-GET requests (GET requests go through navigationFallback), and the Remix adapter overrides the * route for all requests. If I manually edit the local SWA config to remove methods from the * route, I get the header back in all cases.
This adapter: https://github.com/geoffrich/svelte-adapter-azure-swa/blob/6d489634cb692afd9c5d717a60ce37ed8280a0d9/index.js#L177-L181
Remix version: https://github.com/derkoe/remix-azure-functions-demo/blob/f94081582ba57b1045193cb5ada574739858d588/public/staticwebapp.config.json#L9-L12
I'm honestly not sure if there are any downsides to using the * route instead of navigationFallback. Would have to do more testing first and see if it fixes the problem in production too. Looking at the docs, it seems like navigationFallback used to be a * route, I wonder why it changed.
The downside of using the * is that you'll have to define all static URLs as separate route like this:
{
"route": "/favicon.ico"
},
Yep, confirmed in #113 that is the case. So if we wanted to make that switch, we'd have to add routing rules for anything in static and any prerendered files. That's doable, but doesn't seem worth the complexity -- I'd rather see the underlying SWA issue fixed, though IDK what the timeline is on that.
Yep, confirmed in #113 that is the case. So if we wanted to make that switch, we'd have to add routing rules for anything in
staticand any prerendered files. That's doable, but doesn't seem worth the complexity -- I'd rather see the underlying SWA issue fixed, though IDK what the timeline is on that.
I agree the complexity is not ideal, but I have implemented this as a side-effect in #109, FYI
Once alternate approach to force the client principal header in local dev is middleware like this that fetches the /.auth/me endpoint and manually attaches the x-ms-client-principal header (extended from the example earlier)
const swaPlugin = () => ({
name: 'configure-swa-proxy',
/**
* @param {import('vite').ViteDevServer} server
*/
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === '/api/__render') {
const originalUrl = req.headers['x-ms-original-url'];
const parsedUrl = new URL(originalUrl);
const rewrittenUrl = parsedUrl.pathname + parsedUrl.search;
req.url = rewrittenUrl;
req.originalUrl = rewrittenUrl;
}
// not all page requests go through /api/__render, so just filter out source file requests
// though maybe we need a better heuristic, since there could theoretically be a /src route.
if (!req.url.startsWith('/src/') && !req.headers['x-ms-client-principal']) {
fetch('http://localhost:4280/.auth/me', { headers: { cookie: req.headers.cookie } })
.then((r) => r.text())
.then((authResponse) => {
const parsedAuthResponse = JSON.parse(authResponse);
// we are emulating the client principal header, which does not include claims
delete parsedAuthResponse.clientPrincipal.claims;
const clientPrincipalHeader = Buffer.from(
JSON.stringify(parsedAuthResponse.clientPrincipal)
).toString('base64');
req.headers['x-ms-client-principal'] = clientPrincipalHeader;
next();
});
} else {
next();
}
});
}
});
I think the reason we don't get the header consistently in local dev is related to using the SK dev server instead of http://localhost:7071 for the API, but I'm not 100% certain.
Also, as a note for the eventual documentation: we should provide multiple configurations for dev time (point everything at 5173) and "production" (point at the built app). Something like:
{
"configurations": {
"dev": {
},
"preview": {
}
}
}
Which you can run as the first arg to swa start (e.g. swa start dev)