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

[feat] Option to set dev server URL manually for @vite (without touching server.hmr) #83

Closed
mandrasch opened this issue Jul 17, 2022 · 13 comments

Comments

@mandrasch
Copy link

mandrasch commented Jul 17, 2022

Hi, very glad to see an official integration for vite in laravel. Much respect for the work! 👍

I tried to integrate this with DDEV, a local PHP development environment system based on Docker.

Vite runs inside the docker container. The following vite.config.ts is needed to get this working with DDEV router (a reverse proxy. Vite must respond to all network requests via 0.0.0.0:

/* vite.config.ts */
  server: {
    // respond to all network requests
    host: '0.0.0.0',
    strictPort: true,
    port: 3000
  },

For the HTML output of @vite it would be needed to use the following custom URL

https://my-laravel-project.ddev.site:3000

which would result in

<script type="module" src="https://my-laravel-project.ddev.site:3000/@vite/client"></script>

But: For the above posted config larave/vite-plugin currently resolves the following in public/hot, which doesn't work (of course):

http://[::]:3000

I saw that the dev server URL resolve is handled here:

/**
 * Resolve the dev server URL from the server address and configuration.
 */
function resolveDevServerUrl(address: AddressInfo, config: ResolvedConfig): DevServerUrl {
    const configHmrProtocol = typeof config.server.hmr === 'object' ? config.server.hmr.protocol : null
    const clientProtocol = configHmrProtocol ? (configHmrProtocol === 'wss' ? 'https' : 'http') : null
    const serverProtocol = config.server.https ? 'https' : 'http'
    const protocol = clientProtocol ?? serverProtocol

    const configHmrHost = typeof config.server.hmr === 'object' ? config.server.hmr.host : null
    const configHost = typeof config.server.host === 'string' ? config.server.host : null
    const serverAddress = address.family === 'IPv6' ? `[${address.address}]` : address.address
    const host = configHmrHost ?? configHost ?? serverAddress

    return `${protocol}://${host}:${address.port}`
}

I discussed this at length with DDEV users and DDEV maintainer and they gave me the advice that I should not fiddle around with server.hmr-settings in vite.config.ts because this is not suited for a reverse proxy usage. (Tested it, vite doesn't connect when using hmr.host etc.). (Discussion)

Demo repo: https://github.com/mandrasch/ddev-laravel-breeze-vite#readme

Proposal:

When I manually overwrite public/hot with https://my-laravel-project.ddev.site:3000 everything works fine.

Therefore I would like to suggest to introduce another config option to manually overwrite the dev server url output for @vite.

For example innocenzi/laravel-vite offers an manual override with DEV_SERVER_URL in .env / config/vite.php.

Thanks very much in advance, this may be really helpful for other docker use cases as well I guess!
Best regards, Matthias

@mandrasch mandrasch changed the title [feat] Option to set dev server URL manually for @vite (without touching server.hmr / host) [feat] Option to set dev server URL manually for @vite (without touching server.hmr) Jul 17, 2022
@jessarcher
Copy link
Member

Hi @mandrasch,

server.hmr is the offical way of customising Vite to look for the dev server in a different location than it's served, as is the case when using a reverse proxy. This is specifically mentioned in the Vite docs at https://v2.vitejs.dev/config/#server-hmr.

The Laravel Vite plugin also looks at this configuration to determine how to write to the hot file, which makes it consistent with Vite.

I would imagine you'd need at least the following config:

server: {
  // Listen on all addresses inside the container
  host: true,
  hmr: {
    // Force the Vite client to connect via SSL
    // This will also force a "https://" URL in the hot file 
    protocol: 'wss',
    // The host where the Vite dev server can be accessed
    // This will also force this host to be written to the hot file
    host: 'my-laravel-project.ddev.site',
  },
},

The port config is optional, but with Vite 3 changing the default port to 5173, you may want to specify it as we have running inside using Sail.

I'm not sure how your traffic is routed from my-laravel-project.ddev.site:3000 to the Vite server running inside the container, but you may need to config Docker port publishing for this port as well.

@driesvints
Copy link
Member

Closing this as this is more a support question. Please try a support channel if you need help integrating this with Docker.

@mandrasch
Copy link
Author

mandrasch commented Jul 18, 2022

Thanks very much for responding so fast @jessarcher! 👍

I tried your suggestion and it breaks vite responding to requests in DDEVs (reverse proxy) router. Unfortunately I'm not so deep into this to describe why it breaks. Guess the answer is somewhere in https://v2.vitejs.dev/config/#server-hmr (and as I stated other DDEV users advised not to use hmr in the first place).

image

The port exposing works fine. When I manually replace public/hot with the desired URL, vite responds from within the Docker container. Using server.hmr settings breaks this. :/

I'll try a support channel as suggested by @driesvints as well.

@mandrasch
Copy link
Author

mandrasch commented Jul 19, 2022

Got Enzos take on this (via Discord vite - laravel), asked him why he chose DEV_SERVER_URL as feature in his vite integration:

The official integration and mine chose a different approach, where they communicate with PHP from Vite through creating a file on the disk with an URL, while I communicate from PHP to Vite and let the user configure stuff that could have been auto-generated
Each approach has its advantages, mine is to know the development server URL in advance
I think you can still manage to use their Integration by extending Vite and changing how the tags are generated

@mandrasch
Copy link
Author

mandrasch commented Jul 20, 2022

Quick update, details for the above comment for interested readers of this issue.

If I understood Enzo correctly,

This would allow it to work in use cases like DDEV / reverse proxy / Docker with the following vite.config without server.hmr.

/* vite.config.ts */
  server: {
    // respond to all network requests
    host: '0.0.0.0',
    strictPort: true,
    port: 5173
  },

(Background: Others and I had no luck with server.hmr in DDEV. (See DDEV discord discussion).

@torenware
Copy link

torenware commented Aug 4, 2022

Hi @mandrasch,

server.hmr is the offical way of customising Vite to look for the dev server in a different location than it's served, as is the case when using a reverse proxy. This is specifically mentioned in the Vite docs at https://v2.vitejs.dev/config/#server-hmr.

This is a misunderstanding of server.hmr.

server.hmr controls what parameters are passed from the Vite dev server to the client.ts script that initiates the HMR web socket after the vite 'nub' script starts up and loads its assets from the vite dev server. It does not "to look for the dev server in a different location than it's served". It tells client.ts where to look for web socket end point. That's ALL it does.

The Laravel Vite plugin also looks at this configuration to determine how to write to the hot file, which makes it consistent with Vite.

I'm not a Laravel guy. "hot file" is not a Vite term. What does this mean?

I would imagine you'd need at least the following config:

server: {
  // Listen on all addresses inside the container
  host: true,
  hmr: {
    // Force the Vite client to connect via SSL
    // This will also force a "https://" URL in the hot file 
    protocol: 'wss',
    // The host where the Vite dev server can be accessed
    // This will also force this host to be written to the hot file
    host: 'my-laravel-project.ddev.site',
  },
},

Nope. wrong.

The problem is that @vite() need to resolve differently when you are behind a reverse proxy. And server.hmr.host is definitely NOT referenced when @vite resolves to the script and style tags it creates.

See this issue for more info. But your information here is incorrect.

I'm reasonably sure (from knowing Drupal and Symfony internals) that Laravel has access to info from the HTTP request as to what's on the other side of the proxy. You should use that info as defaults in resolving @vite(). In particular, do not do this, which is what you are doing now:

<script type="module" src="[http://[::]:5173/@vite/client](view-source:http://[::]:5173/@vite/client)"></script><link rel="stylesheet" href="[http://[::]:5173/resources/css/app.css](view-source:http://[::]:5173/resources/css/app.css)" /><script type="module" src="[http://[::]:5173/resources/js/app.js](view-source:http://[::]:5173/resources/js/app.js)"></script>

@jessarcher
Copy link
Member

server.hmr is the offical way of customising Vite to look for the dev server in a different location than it's served, as is the case when using a reverse proxy. This is specifically mentioned in the Vite docs at https://v2.vitejs.dev/config/#server-hmr.

This is a misunderstanding of server.hmr.

server.hmr controls what parameters are passed from the Vite dev server to the client.ts script that initiates the HMR web socket after the vite 'nub' script starts up and loads its assets from the vite dev server. It does not "to look for the dev server in a different location than it's served". It tells client.ts where to look for web socket end point. That's ALL it does.

I'm not sure how this is really different from what I said. Perhaps I could have used better terminology:

server.hmr is the offical way of customising Vite the Vite client to look for the web socket end point of the dev server in a different location than it's served.

I'm not a Laravel guy. "hot file" is not a Vite term. What does this mean?

The "hot file" is a plain text file that this plugin writes to public/hot containing the resolved URL of the Vite HMR server. It is deleted when the Vite dev server is stopped. Laravel uses this file to determine if the Vite dev server is running and to know what URL to use as the base for script and link tags.

The problem is that @vite() need to resolve differently when you are behind a reverse proxy. And server.hmr.host is definitely NOT referenced when @vite resolves to the script and style tags it creates.

I'm not sure what version of this plugin you are using, but since v0.3.0 (this PR specifically) we respect server.hmr.host when determining what URL to write to the public/hot file:

vite-plugin/src/index.ts

Lines 340 to 343 in 89d74e4

const configHmrHost = typeof config.server.hmr === 'object' ? config.server.hmr.host : null
const configHost = typeof config.server.host === 'string' ? config.server.host : null
const serverAddress = isIpv6(address) ? `[${address.address}]` : address.address
const host = configHmrHost ?? configHost ?? serverAddress

In particular, do not do this, which is what you are doing now:

<script type="module" src="[http://[::]:5173/@vite/client](view-source:http://[::]:5173/@vite/client)"></script><link rel="stylesheet" href="[http://[::]:5173/resources/css/app.css](view-source:http://[::]:5173/resources/css/app.css)" /><script type="module" src="[http://[::]:5173/resources/js/app.js](view-source:http://[::]:5173/resources/js/app.js)"></script>

I'm not aware of any situation where the @vite() directive would write that. Are those view-source parts actually being output, or is that a bad copy/paste?

@torenware
Copy link

server.hmr is the offical way of customising Vite the Vite client to look for the web socket end point of the dev server in a different location than it's served.

The trouble with most of the settings under server.hmr is that they either have side effects, or the client.ts script can infer what to do correctly, so the settings are irrelevant (this from sapphi-red, who reviewed some of my code on a vite integration).

server.hmr.host is Mostly Harmless, but Vite itself doesn't really need it. Were I developing your integration, I'd hesitate to use it to change Laravel's behavior. In particular, I've seen frustrated Laravel users play with server.hmr.https and server.hmr.port, which just makes their lives worse :-) At most they might want to play with server.hmr.clientPort.

I'm not a Laravel guy. "hot file" is not a Vite term. What does this mean?

The "hot file" is a plain text file that this plugin writes to public/hot containing the resolved URL of the Vite HMR server. It is deleted when the Vite dev server is stopped. Laravel uses this file to determine if the Vite dev server is running and to know what URL to use as the base for script and link tags.

To the file system?? Still haven't figured out where you guys mean with that.

The problem is that @vite() need to resolve differently when you are behind a reverse proxy. And server.hmr.host is definitely NOT referenced when @vite resolves to the script and style tags it creates.

I'm not sure what version of this plugin you are using, but since v0.3.0 (this PR specifically) we respect server.hmr.host when determining what URL to write to the public/hot file:

What version is bundled with Laravel v9.23.0? I don't see a separate entry in composer.lock for the vite plugin.

In v9.23.0, @vite completely ignores server.hmr.host.

vite-plugin/src/index.ts

Lines 340 to 343 in 89d74e4

const configHmrHost = typeof config.server.hmr === 'object' ? config.server.hmr.host : null
const configHost = typeof config.server.host === 'string' ? config.server.host : null
const serverAddress = isIpv6(address) ? `[${address.address}]` : address.address
const host = configHmrHost ?? configHost ?? serverAddress

In particular, do not do this, which is what you are doing now:

<script type="module" src="[http://[::]:5173/@vite/client](view-source:http://[::]:5173/@vite/client)"></script><link rel="stylesheet" href="[http://[::]:5173/resources/css/app.css](view-source:http://[::]:5173/resources/css/app.css)" /><script type="module" src="[http://[::]:5173/resources/js/app.js](view-source:http://[::]:5173/resources/js/app.js)"></script>

I'm not aware of any situation where the @vite() directive would write that. Are those view-source parts actually being output, or is that a bad copy/paste?

Nope. Straight out of the view-source window on Firefox, without any formatting. That is exactly what @Vite() resolves to.

@torenware
Copy link

@jessarcher If you have the chance, take a look at this issue I just posted. It links a repo of a Laravel user I've been working with, which might make it easier for you to reproduce what we're seeing.

@torenware
Copy link

torenware commented Aug 4, 2022

Not sure how the "view-source" stuff ended up in the link. Odd Firefox behavior on copy. Here's what I just pulled off view-source for the page in question, using Chrome instead:

<script type="module" src="http://[::]:5173/@vite/client"></script><link rel="stylesheet" href="http://[::]:5173/resources/css/app.css" /><script type="module" src="http://[::]:5173/resources/js/app.js"></script>
 

http://[::] is wrong. https://HOSTNAME would be right. I think it would be better to look at any proxy headers and get these that way, rather than hacking server.hmr's settings for this purpose. Talking with some of the Vite 3 devs makes me think that the HMR settings are trouble, besides being very badly documented up on Vite's site.

@jessarcher
Copy link
Member

http://[::] is wrong. https://HOSTNAME would be right.

We're using Vite's configureServer hook to listen for when the Vite dev server is listening for connections:

vite-plugin/src/index.ts

Lines 146 to 153 in 89d74e4

configureServer(server) {
const hotFile = path.join(pluginConfig.publicDirectory, 'hot')
const envDir = resolvedConfig.envDir || process.cwd()
const appUrl = loadEnv('', envDir, 'APP_URL').APP_URL
server.httpServer?.once('listening', () => {
const address = server.httpServer?.address()

The configureServer hook gives us an instance of ViteDevServer which we can use to get the underlying Node HTTP server. From there, we can retrieve the address of the server, but it's only an address and not a hostname. Furthermore, if the server is listening on all addresses, then the only address it gives us is the meta "all addresses" address (:: or 0.0.0.0).

This plugin only has access to what Vite gives us and what the user configures. It has no knowledge of hostnames outside of that.

I think it would be better to look at any proxy headers and get these that way, rather than hacking server.hmr's settings for this purpose.

How would Laravel Framework integration determine the appropriate Vite server address, including the port, from the proxy headers?

I've spun up the project you mentioned and can confirm that by default it tries to retrieve the assets from [::], which is expected, but does not work with this scenario:

image

If I use the following configuration, then it will successfully make the request to the intended location:

server: {
    hmr: {
        protocol: 'wss',
        host: 'my-laravel-app.ddev.site',
    },
},

image

I'm not sure what the pnpm error is - I'm not using pnpm though.

@torenware
Copy link

torenware commented Aug 4, 2022

Not sure what the pnpm error is either, but probably not our problem, at least here :-)

I can confirm that with the hmr settings you recommend, that the tags now resolve to:

<script type="module" src="https://my-laravel-app.ddev.site:5173/@vite/client"></script><link rel="stylesheet" href="https://my-laravel-app.ddev.site:5173/resources/css/app.css" /><script type="module" src="https://my-laravel-app.ddev.site:5173/resources/js/app.js"></script>
 

Also, HMR comes up correctly, which means we have the thing working.

As for:

How would Laravel Framework integration determine the appropriate Vite server address, including the port, from the proxy headers?

It's been a few years since I've played with these, but X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host get transmitted across the reverse proxy IIRC. You'd want the latter two. I'm not sure what PHP or Symfony's request handling (which IIRC Laravel uses, as Drupal and Symfony do) does with these. But you'd get the port as you do now, and pull host and protocol from the X-Forwarded headers.

@jessarcher
Copy link
Member

It feels like a bit of a stretch for Laravel to assume that the proxy it's behind is also where it will find Vite, only on a different port. I'd imagine there are as many users where this is not the case as there are where it is.

I do agree that using server.hmr.host is a bit hacky, as the documentation specifically mentions that this is for locating the HMR websocket server, rather than the HMR HTTP server which is what we're using it for. The Laravel integration is in a bit of a tricky spot here. With Vite's primary use case, the user is browsing directly to Vite's HMR HTTP server to see their app, but in our case the user is browsing to Laravel's server which then needs to load scripts from Vite's HMR HTTP server, which in turn establishes a connection to Vite's HMR websocket server. I'm assuming that for 99% of Laravel users, the HMR HTTP and websocket servers will be one and the same.

server.hmr.host is not the primary way we determine the server location though. By default it just tries to load the scripts from wherever the ViteDevServer.httpServer tells us it's listening, which seems like a safe default. If the user has configured server.host or server.hmr.host then we'll use that. We only document server.host for a specific scenario, and we don't document server.hmr.host at all, as it's really just an escape hatch for advanced use cases like this.

Users are of course also free to use other plugins like laravel-vite.dev or even just configure things themselves. Our primary focus is to make sure things work well with Laravel's first-party dev environments like Valet and Sail.

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

4 participants