Skip to content

Commit

Permalink
feat(cli): create plugin for handling application api calls (#2448)
Browse files Browse the repository at this point in the history
- generate manifest, config and source code when local app
- proxy app-service calls when app miss-match
  • Loading branch information
odinr authored Sep 12, 2024
1 parent 1863a02 commit 870713c
Show file tree
Hide file tree
Showing 2 changed files with 233 additions and 0 deletions.
66 changes: 66 additions & 0 deletions .changeset/quiet-scissors-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
'@equinor/fusion-framework-cli': minor
---

**@equinor/fusion-framework-cli**

The `appProxyPlugin` is a Vite plugin designed to proxy requests to a Fusion app backend.
It sets up proxy rules for API and bundle requests and serves the app configuration and manifest based on the app key and version.

Key Features:

1. Proxy Configuration:

- Proxies API calls to the Fusion apps backend.
- Proxies bundle requests to the Fusion apps backend.
- Uses a base path `proxyPath` for proxying.
- Captures and reuses authorization tokens for asset requests.

2. **App Configuration and Manifest**:

- Serves the app configuration if the request matches the current app and version.
- Serves the app manifest if the request matches the current app.

3. **Middleware Setup**:
- Sets up middleware to handle requests for app configuration, manifest, and local bundles.

This plugin is used by the CLI for local development, but design as exportable for custom CLI to consume applications from other API`s

example configuration:
```typescript
const viteConfig = defineConfig({
// vite configuration
plugins: [
appProxyPlugin({
proxy: {
path: '/app-proxy',
target: 'https://fusion-s-apps-ci.azurewebsites.net/',
// optional callback when matched request is proxied
onProxyReq: (proxyReq, req, res) => {
proxyReq.on('response', (res) => { console.log(res.statusCode) });
},
},
// optional, but required for serving local app configuration, manifest and resources
app: {
key: 'my-app',
version: '1.0.0',
generateConfig: async () => ({}),
generateManifest: async () => ({}),
},
}),
],
});
```

example usage:
```typescript
// Example API calls
fetch('/app-proxy/apps/my-app/builds/1.0.0/config'); // local
fetch('/app-proxy/apps/my-app/builds/0.0.9/config'); // proxy
fetch('/app-proxy/apps/other-app/builds/1.0.0/config'); // proxy

// Example asset calls
fetch('/app-proxy/bundles/my-app/builds/1.0.0/index.js'); // local
fetch('/app-proxy/bundles/my-app/builds/0.0.9/index.js'); // proxy
```

167 changes: 167 additions & 0 deletions packages/cli/src/bin/plugins/app-proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { Plugin } from 'vite';

import { assert } from 'node:console';

import { AppConfig, ApplicationManifest } from '@equinor/fusion-framework-app';
import { ClientRequest, IncomingMessage, ServerResponse } from 'node:http';

/**
* Preserve token for executing proxy assets
*
* @remarks
* This assumes the client will execute a api call using bearer token before
* acquiring a asset. By default the Framework will execute a rest call to load
* application manifest for resolving build assets to import.
*
* @remarks
* This is a quick and dirty method to authorize requests without bearer token
* like browser `import`.
* The correct way would be to have a auth controller within the dev-server,
* but since the token is only exposed to the plugin and the cli is a tool for local
* development, this should be sufficient.
*/
let __APP_API_TOKEN__ = '';

/**
* Options for configuring the App Proxy Plugin.
*
* @remarks
* When not providing an app configuration, the plugin will only proxy requests to the target.
*/
export type AppProxyPluginOptions = {
/** Configuration for the proxy. */
proxy: {
/** The path to be proxied. */
path: string;
/** The target URL for the proxy. */
target: string;
/** Optional callback function to modify the proxy request. */
onProxyReq?: (proxyReq: ClientRequest, req: IncomingMessage, res: ServerResponse) => void;
};
/** Optional configuration for the app. */
app?: {
/** application key */
key: string;
/** application version */
version: string;
/** callback function for generating configuration for the application */
generateConfig: () => Promise<AppConfig>;
/** callback function for generating manifest for the application */
generateManifest: () => Promise<ApplicationManifest>;
};
};

/**
* The `appProxyPlugin` function creates a Vite plugin that configures a proxy for API and bundle requests
* to the Fusion apps backend. It also serves the app manifest, config, and local bundles if an app is provided.
*
* @param {AppProxyPluginOptions} options - The options for configuring the app proxy plugin.
*
* @returns {Plugin} - The configured Vite plugin.
*
* @example
* ```typescript
* const plugin = appProxyPlugin({
* proxy: {
* path: '/app-proxy',
* target: 'https://fusion-s-apps-ci.azurewebsites.net/',
* onProxyReq: (proxyReq, req, res) => {
* proxyReq.on('response', (res) => { console.log(res.statusCode) });
* },
* },
* app: {
* key: 'my-app',
* version: '1.0.0',
* generateConfig: async () => ({}),
* generateManifest: async () => ({}),
* },
* });
*
* // api calls
* fetch('/app-proxy/apps/my-app/builds/1.0.0/config'); // will generate app config by provided function
* fetch('/app-proxy/apps/my-app/builds/0.0.9/config'); // will proxy to the target, since version does not match
* fetch('/app-proxy/apps/other-app/builds/1.0.0/config'); // will proxy to the target, since app key does not match
*
* // asset calls
* fetch('/app-proxy/bundles/my-app/builds/1.0.0/index.js'); // will generate bundle by provided function
* fetch('/app-proxy/bundles/my-app/builds/0.0.9/index.js'); // will proxy to the target, since version does not match
* ```
*
*/
export const appProxyPlugin = (options: AppProxyPluginOptions): Plugin => {
const {
proxy: { onProxyReq = () => void 0, path: proxyPath, target },
} = options;
return {
name: 'fusion:app-proxy',
apply: 'serve',
config(config) {
config.server ??= {};
config.server.proxy = {
// proxy all api calls to the fusion apps backend
[`${proxyPath}/apps`]: {
target,
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(proxyPath, ''),
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq) => {
const token = proxyReq.getHeader('authorization');
if (typeof token === 'string') {
__APP_API_TOKEN__ = token;
}
});
proxy.on('proxyReq', onProxyReq);
},
},
// proxy all bundle requests to the fusion apps backend
[`${proxyPath}/bundles`]: {
target,
changeOrigin: true,
secure: false,
rewrite: (path) => path.replace(proxyPath, ''),
configure: (proxy) => {
proxy.on('proxyReq', (proxyReq) => {
assert(__APP_API_TOKEN__, 'expected token to be set');
proxyReq.setHeader('authorization', __APP_API_TOKEN__);
});
proxy.on('proxyReq', onProxyReq);
},
},
};
},
configureServer(server) {
const { app } = options;

// disable local assets if no app configuration provided
if (!app) return;

// serve app manifest if request matches the current app
// todo this should have version
const manifestPath = `${proxyPath}/apps/${app.key}`;
server.middlewares.use(manifestPath, async (_req, res) => {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(await app.generateManifest()));
});

// serve app config if request matches the current app and version
const configPath = `${proxyPath}/apps/${app.key}/builds/${app.version}/config`;
server.middlewares.use(configPath, async (_req, res) => {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify(await app.generateConfig()));
});

// serve local bundles if request matches the current app and version
const bundlePath = `${proxyPath}/bundles/apps/${app.key}@${app.version}`;
server.middlewares.use(async (req, _res, next) => {
if (req.url?.match(bundlePath)) {
// remove proxy path from url
req.url = req.url!.replace(bundlePath, '');
}
next();
});
},
};
};

export default appProxyPlugin;

0 comments on commit 870713c

Please sign in to comment.