Skip to content

Commit

Permalink
Handle Vite plugin injections in Custom Widget Vite plugin (#1063)
Browse files Browse the repository at this point in the history
* Fixes for dev mode against initial template

Add comments

Remove logging

* changeset

* correct try block location

---------

Co-authored-by: Ross Court <[email protected]>
  • Loading branch information
rosscourt and Ross Court authored Dec 13, 2024
1 parent 04d5e22 commit e1649c2
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 54 deletions.
5 changes: 5 additions & 0 deletions .changeset/long-frogs-reflect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@osdk/widget-manifest-vite-plugin": patch
---

Handle Vite plugin injections in Custom Widget Vite plugin
1 change: 1 addition & 0 deletions packages/widget.vite-plugin/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@

export const PALANTIR_PATH = ".palantir";
export const SETUP_PATH = `${PALANTIR_PATH}/setup`;
export const VITE_INJECTIONS = `${PALANTIR_PATH}/vite-injections.js`;
229 changes: 175 additions & 54 deletions packages/widget.vite-plugin/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,15 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import color from "picocolors";
import sirv from "sirv";
import type { Plugin, ResolvedConfig, ViteDevServer } from "vite";
import { PALANTIR_PATH, SETUP_PATH } from "./constants.js";
import type {
HtmlTagDescriptor,
IndexHtmlTransformHook,
IndexHtmlTransformResult,
Plugin,
ResolvedConfig,
ViteDevServer,
} from "vite";
import { PALANTIR_PATH, SETUP_PATH, VITE_INJECTIONS } from "./constants.js";

export const DIR_DIST = typeof __dirname !== "undefined"
? __dirname
Expand Down Expand Up @@ -141,6 +148,21 @@ export function FoundryWidgetVitePlugin(options: Options = {}): Plugin {
},
);

server.middlewares.use(
`${server.config.base ?? "/"}${VITE_INJECTIONS}`,
async (req, res) => {
if (devServer == null) {
res.statusCode = 500;
res.statusMessage = "Vite server not found.";
res.end();
return;
}
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Content-Type", "application/javascript");
res.end(await extractInjectedScripts(devServer));
},
);

// Called by the setup page to start dev mode in Foundry, which queries the appropriate service on the Foundry instance configured in foundry.config.json
server.middlewares.use(
`${server.config.base ?? "/"}${PALANTIR_PATH}/finish`,
Expand All @@ -159,7 +181,7 @@ export function FoundryWidgetVitePlugin(options: Options = {}): Plugin {
body += readResult;
}
});
req.on("end", () => {
req.on("end", async () => {
if (body.length === 0) {
res.statusCode = 400;
res.statusMessage =
Expand Down Expand Up @@ -216,9 +238,9 @@ export function FoundryWidgetVitePlugin(options: Options = {}): Plugin {
return;
}

let url: URL;
let foundryUrl: URL;
try {
url = new URL(foundryConfig.foundryUrl);
foundryUrl = new URL(foundryConfig.foundryUrl);
} catch (error) {
res.statusCode = 500;
res.statusMessage =
Expand All @@ -235,57 +257,52 @@ export function FoundryWidgetVitePlugin(options: Options = {}): Plugin {
return;
}

// TODO: Actually handle the widget RID from within the config, which will require somehow parsing the config
// Unfortunately, moduleParsed is not called during vite's dev mode for performance reasons, so the config file
// will need to be parsed/read a different way
fetch(
`${url.origin}/view-registry/api/dev-mode/${foundryConfig.widget.rid}/settings`,
{
body: JSON.stringify({
entrypointJs: [
...entrypointToJsSourceFileMap[entrypointFileName],
].map((file) => ({
scriptType: {
type: "module",
module: {},
},
filePath: `${localhostUrl}${file}`,
})),
entrypointCss: [],
}),
method: "POST",
headers: {
authorization: `Bearer ${process.env.FOUNDRY_TOKEN}`,
accept: "application/json",
"content-type": "application/json",
},
},
)
.then((devResponse) => {
if (devResponse.status === 200 || devResponse.status === 204) {
res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify({
redirectUrl:
`${url.origin}/workspace/custom-views/preview/${foundryConfig.widget.rid}`,
}),
);
} else {
res.statusCode = devResponse.status;
res.statusMessage =
`Unable to start dev mode in Foundry (see terminal for more): ${devResponse.statusText}`;
devResponse.text().then((err) => {
config?.logger.error(err);
res.end();
});
}
})
.catch((error) => {
res.statusCode = 500;
try {
const settingsResponse = await setWidgetSettings(
// TODO: Actually handle the widget RID from within the config, which will require somehow parsing the config
// Unfortunately, moduleParsed is not called during vite's dev mode for performance reasons, so the config file
// will need to be parsed/read a different way
foundryConfig.widget.rid,
foundryUrl,
localhostUrl,
entrypointToJsSourceFileMap,
entrypointFileName,
);
if (
settingsResponse.status !== 200
) {
res.statusCode = settingsResponse.status;
res.statusMessage =
`Unable to set widget settings in Foundry: ${settingsResponse.statusText}`;
settingsResponse.text().then((err) => {
config?.logger.error(err);
res.end();
});
return;
}

const enableResponse = await enableDevMode(foundryUrl);
if (enableResponse.status !== 200) {
res.statusCode = enableResponse.status;
res.statusMessage =
`Unable to start dev mode in Foundry: ${error.message}`;
`Unable to start dev mode in Foundry: ${enableResponse.statusText}`;
res.end();
});
return;
}

res.setHeader("Content-Type", "application/json");
res.end(
JSON.stringify({
redirectUrl:
`${foundryUrl.origin}/workspace/custom-views/preview/${foundryConfig.widget.rid}`,
}),
);
} catch (error: any) {
res.statusCode = 500;
res.statusMessage =
`Unable to start dev mode in Foundry: ${error.message}`;
res.end();
}
});
},
);
Expand Down Expand Up @@ -572,3 +589,107 @@ function extractWidgetConfig(objectExpression: ObjectExpression) {

return JSON.parse(widgetConfigString);
}

function setWidgetSettings(
widgetRid: string,
foundryUrl: URL,
localhostUrl: string,
entrypointToJsSourceFileMap: Record<string, Set<string>>,
entrypointFileName: string,
) {
const widgetDevModeSettings = {
entrypointJs: [
`/${VITE_INJECTIONS}`,
"/@vite/client",
...entrypointToJsSourceFileMap[entrypointFileName],
].map((file) => ({
filePath: `${localhostUrl}${file}`,
scriptType: { type: "module", module: {} },
})),
entrypointCss: [],
};
return fetch(
`${foundryUrl.origin}/view-registry/api/dev-mode/settings/${widgetRid}`,
{
body: JSON.stringify(widgetDevModeSettings),
method: "PUT",
headers: {
authorization: `Bearer ${process.env.FOUNDRY_TOKEN}`,
accept: "application/json",
"content-type": "application/json",
},
},
);
}

function enableDevMode(foundryUrl: URL) {
return fetch(`${foundryUrl.origin}/view-registry/api/dev-mode/enable`, {
method: "POST",
headers: {
authorization: `Bearer ${process.env.FOUNDRY_TOKEN}`,
accept: "application/json",
},
});
}

/**
* Extracts inline scripts injected by Vite plugins during HTML transformation.
*
* Vite plugins can inject scripts into the HTML entrypoint. This function captures
* those injections, specifically inline scripts, which are needed for our server-side
* rendered pages. It calls the `transformIndexHtml` hook on each plugin, collects
* the script descriptors, and returns the concatenated inline script contents.
*
* See documentation: https://vite.dev/guide/api-plugin#transformindexhtml
*/
async function extractInjectedScripts(
devServer: ViteDevServer,
): Promise<string> {
const pluginTransforms = (devServer?.pluginContainer.plugins ?? [])
.map(getPluginTransformHook)
.filter((hook): hook is IndexHtmlTransformHook => hook != null);
const transformResults: Array<IndexHtmlTransformResult | undefined> =
await Promise.all(
pluginTransforms.map(async (transformHook) => {
// The parameters to the transform hook are not used in the cases we currently support
const result = await transformHook("", { path: "", filename: "" });
return result ?? undefined;
}),
);

// We are only interested in extracting scripts that Vite would usually inject as inline scripts
const inlineScriptDescriptors: HtmlTagDescriptor[] = transformResults
.filter((result): result is HtmlTagDescriptor[] =>
result != null && typeof result !== "string"
)
.flat()
.filter((descriptor: HtmlTagDescriptor) =>
descriptor.tag === "script" && typeof descriptor.children === "string"
);

return inlineScriptDescriptors.map((descriptor) => descriptor.children).join(
"\n",
);
}

/**
* The Vite plugin API for transformIndexHtml supports a few different formats,
* all which ultimately resolve to the function that we want to call.
*/
function getPluginTransformHook(
plugin: Plugin<unknown>,
): IndexHtmlTransformHook | undefined {
if (typeof plugin.transformIndexHtml === "function") {
return plugin.transformIndexHtml;
} else if (
typeof plugin.transformIndexHtml === "object"
&& "handler" in plugin.transformIndexHtml
) {
return plugin.transformIndexHtml.handler;
} else if (
typeof plugin.transformIndexHtml === "object"
&& "transform" in plugin.transformIndexHtml
) {
return plugin.transformIndexHtml.transform;
}
}

0 comments on commit e1649c2

Please sign in to comment.