Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Investigate improving SWA CLI story #96

Open
geoffrich opened this issue Oct 31, 2022 · 17 comments
Open

Investigate improving SWA CLI story #96

geoffrich opened this issue Oct 31, 2022 · 17 comments
Milestone

Comments

@geoffrich
Copy link
Owner

geoffrich commented Oct 31, 2022

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.

@derkoe
Copy link

derkoe commented Oct 31, 2022

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

@geoffrich
Copy link
Owner Author

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)

@geoffrich
Copy link
Owner Author

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

@derkoe
Copy link

derkoe commented Jan 24, 2023

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

@tlaundal
Copy link
Contributor

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.

@geoffrich
Copy link
Owner Author

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.

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.

@geoffrich
Copy link
Owner Author

@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 #102 (comment) for more details.

@tlaundal
Copy link
Contributor

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.

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.

@tlaundal
Copy link
Contributor

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.

@derkoe
Copy link

derkoe commented Jan 24, 2023

@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.

@tlaundal
Copy link
Contributor

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

@geoffrich
Copy link
Owner Author

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:

{
route: '*',
methods: ['POST', 'PUT', 'DELETE'],
rewrite: ssrFunctionRoute
},

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.

image

@derkoe
Copy link

derkoe commented Jan 25, 2023

The downside of using the * is that you'll have to define all static URLs as separate route like this:

    {
      "route": "/favicon.ico"
    },

@geoffrich
Copy link
Owner Author

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.

@tlaundal
Copy link
Contributor

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.

I agree the complexity is not ideal, but I have implemented this as a side-effect in #109, FYI

@geoffrich
Copy link
Owner Author

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.

@geoffrich
Copy link
Owner Author

geoffrich commented Jan 28, 2023

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants