-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli): create plugin for handling application api calls (#2448)
- generate manifest, config and source code when local app - proxy app-service calls when app miss-match
- Loading branch information
Showing
2 changed files
with
233 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |