add proxy support

This commit is contained in:
Michał Sieciechowicz 2026-01-18 23:14:41 +01:00
parent 72f5d249cf
commit ebb413d693
3 changed files with 176 additions and 0 deletions

View File

@ -113,6 +113,14 @@ bootstrapApplication(AppComponent, {
} }
} }
}, },
"serve": {
"proxy": {
"/api/*": {
"target": "http://localhost:3000",
"changeOrigin": true
}
}
},
"environments": { "environments": {
"development": { "development": {
"minifyNames": true, "minifyNames": true,
@ -329,6 +337,84 @@ export class HighlightDirective {
## 🔧 Advanced Features ## 🔧 Advanced Features
### Development Server Proxy
The development server supports proxying HTTP requests to a backend server. This is useful when your frontend needs to communicate with an API during development.
**Configuration in quarc.json:**
```json
{
"serve": {
"proxy": {
"/api/*": {
"target": "http://192.168.1.100:8080",
"changeOrigin": true
},
"/auth/*": {
"target": "https://auth.example.com",
"changeOrigin": true,
"pathRewrite": {
"^/auth": "/api/v1/auth"
}
}
}
}
}
```
**Proxy Options:**
- **Pattern matching**: Use wildcards (`*`) to match request paths
- `/api/*` matches `/api/users`, `/api/data`, etc.
- `/v1/*/data` matches `/v1/users/data`, `/v1/products/data`, etc.
- **target**: Backend server URL (required)
- Can be HTTP or HTTPS
- Include protocol and host, optionally port
- **changeOrigin**: Set to `true` to change the `Host` header to match the target (recommended for most cases)
- **pathRewrite**: Object with regex patterns to rewrite request paths
- Key: regex pattern to match
- Value: replacement string
**Example Use Cases:**
```json
{
"serve": {
"proxy": {
"/api/*": {
"target": "http://localhost:3000",
"changeOrigin": true
}
}
}
}
```
When your app makes a request to `http://localhost:4200/api/users`, it will be proxied to `http://localhost:3000/api/users`.
**Perfect for ESP32 Development:**
When developing for ESP32, you can proxy API requests to the device:
```json
{
"serve": {
"proxy": {
"/api/*": {
"target": "http://192.168.1.50",
"changeOrigin": true
}
}
}
}
```
This allows you to develop your frontend locally while testing against the actual ESP32 backend.
### Lazy Loading ### Lazy Loading
Components are automatically lazy-loaded when using route-based code splitting: Components are automatically lazy-loaded when using route-based code splitting:

View File

@ -10,6 +10,7 @@ import { BaseBuilder } from './base-builder';
import { import {
StaticPath, StaticPath,
StaticRemotePath, StaticRemotePath,
ProxyConfig,
} from '../types'; } from '../types';
class Server extends BaseBuilder { class Server extends BaseBuilder {
@ -159,6 +160,82 @@ class Server extends BaseBuilder {
return this.config.serve?.staticPaths || []; return this.config.serve?.staticPaths || [];
} }
private getProxyConfig(): ProxyConfig {
return this.config.serve?.proxy || {};
}
private matchProxyPath(reqUrl: string): { pattern: string; config: { target: string; changeOrigin?: boolean; pathRewrite?: { [key: string]: string } } } | null {
const proxyConfig = this.getProxyConfig();
for (const [pattern, config] of Object.entries(proxyConfig)) {
const regexPattern = pattern.replace(/\*/g, '.*');
const regex = new RegExp(`^${regexPattern}`);
if (regex.test(reqUrl)) {
return { pattern, config };
}
}
return null;
}
private tryProxyRequest(reqUrl: string, req: http.IncomingMessage, res: http.ServerResponse): boolean {
const match = this.matchProxyPath(reqUrl);
if (!match) {
return false;
}
const { pattern, config } = match;
let targetPath = reqUrl;
if (config.pathRewrite) {
for (const [from, to] of Object.entries(config.pathRewrite)) {
const fromRegex = new RegExp(from);
targetPath = targetPath.replace(fromRegex, to);
}
}
const targetUrl = config.target + targetPath;
if (this.isVerbose()) {
console.log(`[Proxy] ${req.method} ${reqUrl} -> ${targetUrl}`);
}
const parsedUrl = new URL(targetUrl);
const protocol = parsedUrl.protocol === 'https:' ? https : http;
const headers: http.OutgoingHttpHeaders = { ...req.headers };
if (config.changeOrigin) {
headers.host = parsedUrl.host;
}
const proxyReq = protocol.request(
targetUrl,
{
method: req.method,
headers,
},
(proxyRes) => {
if (this.isVerbose()) {
console.log(`[Proxy] Response: ${proxyRes.statusCode} for ${req.url}`);
}
res.writeHead(proxyRes.statusCode || 500, proxyRes.headers);
proxyRes.pipe(res);
},
);
proxyReq.on('error', (err) => {
console.error(`[Proxy] Error for ${req.url}:`, err.message);
res.writeHead(502);
res.end('Bad Gateway');
});
req.pipe(proxyReq);
return true;
}
private proxyRequest(targetUrl: string, req: http.IncomingMessage, res: http.ServerResponse): void { private proxyRequest(targetUrl: string, req: http.IncomingMessage, res: http.ServerResponse): void {
console.log(`[Proxy] ${req.method} ${req.url} -> ${targetUrl}`); console.log(`[Proxy] ${req.method} ${req.url} -> ${targetUrl}`);
const parsedUrl = new URL(targetUrl); const parsedUrl = new URL(targetUrl);
@ -245,6 +322,10 @@ class Server extends BaseBuilder {
this.httpServer = http.createServer((req, res) => { this.httpServer = http.createServer((req, res) => {
const reqUrl = req.url || '/'; const reqUrl = req.url || '/';
if (this.tryProxyRequest(reqUrl, req, res)) {
return;
}
if (this.tryServeStaticPath(reqUrl, req, res)) { if (this.tryServeStaticPath(reqUrl, req, res)) {
return; return;
} }

View File

@ -32,6 +32,14 @@ export interface StaticRemotePath {
export type StaticPath = StaticLocalPath | StaticRemotePath; export type StaticPath = StaticLocalPath | StaticRemotePath;
export interface ProxyConfig {
[path: string]: {
target: string;
changeOrigin?: boolean;
pathRewrite?: { [key: string]: string };
};
}
export interface ActionsConfig { export interface ActionsConfig {
prebuild?: string[]; prebuild?: string[];
postbuild?: string[]; postbuild?: string[];
@ -57,6 +65,7 @@ export interface BuildConfig {
export interface ServeConfig { export interface ServeConfig {
actions?: ActionsConfig; actions?: ActionsConfig;
staticPaths?: StaticPath[]; staticPaths?: StaticPath[];
proxy?: ProxyConfig;
} }
export interface QuarcConfig { export interface QuarcConfig {