Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add global or import scripts into DOM #1030

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
4 changes: 2 additions & 2 deletions apps/kitchen-sink/src/ensemble/scripts/common.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const productTitleName = "SgrDvr";

const getDateLabel = (val) => {
return `i am a date label ${val}`
}
return `i am a date label ${val}`;
};
57 changes: 32 additions & 25 deletions packages/framework/src/evaluate/evaluate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { isEmpty, merge, toString } from "lodash-es";
import {
get,
has,
isEmpty,
isUndefined,
merge,
omitBy,
toString,
} from "lodash-es";
import type { ScreenContextDefinition } from "../state/screen";
import type { InvokableMethods, WidgetState } from "../state/widget";
import {
Expand All @@ -19,6 +27,10 @@ export const widgetStatesToInvokables = (widgets: {
});
};

interface InvokableWindow extends Window {
[key: string]: unknown;
}

export const buildEvaluateFn = (
screen: Partial<ScreenContextDefinition>,
js?: string,
Expand All @@ -38,14 +50,27 @@ export const buildEvaluateFn = (
// Need to filter out invalid JS identifiers
].filter(([key, _]) => !key.includes(".")),
);
const globalBlock = screen.model?.global;
const importedScriptBlock = screen.model?.importedScripts;

if (has(invokableObj, "ensemble")) {
const tempEnsemble = get(invokableObj, "ensemble") as {
[key: string]: unknown;
};
(window as unknown as InvokableWindow).ensemble = omitBy(
tempEnsemble,
isUndefined,
);
}

Comment on lines +54 to +62
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be set once - most of the functions in ensemble API do not require any screen params. The only thing would be the modal context, which can be passed in as an argument.

const args = Object.keys(invokableObj).join(",");

const combinedJs = `
return evalInClosure(() => {
${formatJs(js)}
}, {${args}})
`;

// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
const jsFunc = new Function(
...Object.keys(invokableObj),
addScriptBlock(formatJs(js), globalBlock, importedScriptBlock),
);
const jsFunc = new Function(...Object.keys(invokableObj), combinedJs);

// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return () => jsFunc(...Object.values(invokableObj));
Expand Down Expand Up @@ -80,24 +105,6 @@ const formatJs = (js?: string): string => {
return `return ${sanitizedJs}`;
};

const addScriptBlock = (
js: string,
globalBlock?: string,
importedScriptBlock?: string,
): string => {
let jsString = ``;

if (importedScriptBlock) {
jsString += `${importedScriptBlock}\n\n`;
}

if (globalBlock) {
jsString += `${globalBlock}\n\n`;
}

return (jsString += `${js}`);
};

/**
* @deprecated Consider using useEvaluate or createBinding which will
* optimize creating the evaluation context
Expand Down
46 changes: 46 additions & 0 deletions packages/runtime/src/runtime/screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,52 @@ export const EnsembleScreen: React.FC<EnsembleScreenProps> = ({
};
}, [screen.customWidgets]);

useEffect(() => {
const globalBlock = screen.global;
const importedScripts = screen.importedScripts;

const isScriptExist = document.getElementById("custom-scope-script");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be unique per screen.


const jsString = `
// Create a base object and pin its reference
const ensembleObj = {};
Object.defineProperty(window, 'ensemble', {
get: () => ensembleObj,
set: (value) => {
// Copy properties instead of replacing reference
Object.assign(ensembleObj, value);
return true;
},
configurable: true,
enumerable: true
});

const createEvalClosure = () => {
${importedScripts || ""}
${globalBlock || ""}

return (scriptToExecute, context) => {
with (context) {
return eval('(' + scriptToExecute.toString() + ')()');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can just be scriptToExecute()

}
}
}

const evalInClosure = createEvalClosure()
`;

if (isScriptExist) {
isScriptExist.textContent = jsString;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not rerun the script, you need to remove the node and reinsert it.

} else {
const script = document.createElement("script");
script.id = "custom-scope-script";
script.type = "text/javascript";
script.textContent = jsString;

document.body.appendChild(script);
}
}, [screen.global, screen.importedScripts]);

if (!isInitialized) {
return null;
}
Expand Down
Loading