analogjs / analog

The fullstack meta-framework for Angular. Powered by Vite and Nitro
https://analogjs.org
MIT License
2.48k stars 234 forks source link

[Proposal] Infer vite's dev-server port on SSR to leverage [ngSrc]'s full path automatically #995

Closed renatoaraujoc closed 1 month ago

renatoaraujoc commented 4 months ago

Which scope/s are relevant/related to the feature request?

platform

Information

Hello,

Currently, when working with SSR locally on Vite DevServer, there's no way to infer the baseURL to prepend requests for the assets given that Domino can't figure out what's going on.

When debugging Angular's source code, window.location.href is resolved to / which is used to construct the asset url like new URL(assetPath, window.location.href). Right now, if you just throw a relative path like /assets/image.png the server will throw an exception and that's it.

In production things are easier because we can provide the imageLoader, no big deal.

Right now, if you dont supply a full path like http://localhost:{PORT}/assets/image.png, SSR will throw an error when using img[ngSrc].

Describe any alternatives/workarounds you're currently using

What I do today for development purposes

  1. Tell Vite's server that we want to work with specific port and it should be strict.
  2. If we're in development, provide an environment variable like DEV_PORT.

file: vite.config.ts

// ...redacted
server: {
    port: 4202,
    strictPort: true
},
define: {
    ...(mode === 'development'
        ? {
              'import.meta.env.DEV_PORT': '4202'
          }
        : {}),
    'import.meta.vitest': mode !== 'production'
}
// ...redacted
  1. In my assetPath function, test if we're running in SSR and if there's a DEV_PORT defined and if so, return the full local url so SSR can successfully fetch the image.

file: assets-path.ts

export const assetPath = (file: string) => {
    if (import.meta.env.SSR && !!import.meta.env?.['DEV_PORT']) {
        return `http://localhost:${import.meta.env.DEV_PORT}/assets/${file}`;
    }

    return `/assets/${file}`;
};

This works but it's hackish (at least for me).

Proposal

Some ideas came to mind:

  1. Vite's analog-plugin could leverage some of this and provide the DEV_PORT automatically like I'm doing, ports can be randomly assigned, so this would make things less hardcoded.

  2. Or analog-plugin could receive a new property like: { ssrBaseUrl: 'INFER' | (string & {}) }. 2.1 if a string is passed, its supposed to be a full base url like: 'http://localhost:4200' or; 2.2 if 'INFER' is passed, analog plugin could somehow infer internally the dev-server port and somehow inject the value http://localhost:{VITE_DEV_PORT} into window.location.href in Domino (or some other magic) so everything would work out of the box.

These are my main ideas, I'm happy to hear about you guys :)

PS: I can't provide any PR for this today because I have 29 days to finish a gigant project, but I felt it was worth it to bring this discussion here.

Renato

I would be willing to submit a PR to fix this issue

osnoser1 commented 2 months ago

I had this same problem, but the problem I could see was the url param in the server method inside of main.server.ts, that URL should be absolute, not only the URL path; seeing here https://github.com/analogjs/analog/blob/beta/packages/vite-plugin-nitro/src/lib/plugins/dev-server-plugin.ts#L51, the req and res object are sent in a third parameter, and I was able to solve the ngSrc problem with this update:

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server';
import { renderApplication } from '@angular/platform-server';
import { enableProdMode } from '@angular/core';
import { IncomingMessage } from 'node:http';

if (import.meta.env.PROD) {
  enableProdMode();
}

const bootstrap = () => bootstrapApplication(AppComponent, config);

export default async function render(
  _path: string,
  document: string,
  { req }: { req: IncomingMessage & { originalUrl: string } },
) {
  const protocol = getRequestProtocol(req);
  const { originalUrl, headers } = req;

  return await renderApplication(bootstrap, {
    document,
    url: `${protocol}://${headers.host}${originalUrl}`,
  });
}

export function getRequestProtocol(
  req: IncomingMessage,
  opts: { xForwardedProto?: boolean } = {},
) {
  if (
    opts.xForwardedProto !== false &&
    req.headers['x-forwarded-proto'] === 'https'
  ) {
    return 'https';
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  return (req.connection as any)?.encrypted ? 'https' : 'http';
}
brandonroberts commented 2 months ago

Interesting find @osnoser1! If this works when deployed also, we could probably provide this in the object with req and res as baseUrl. This could be an opportunity to use it server side also with an interceptor so you don't have to prefix HttpClient requests with the full baseUrl

osnoser1 commented 2 months ago

Awesome 😄

I'm fixing the problems I found when integrating analog with the current app I'm working on. When I deploy it, I'll test it and let you know if this change worked