From c786d14eac84d64f6723fc9f8eb0e00e2251dcfc Mon Sep 17 00:00:00 2001 From: Mantra Date: Thu, 19 Dec 2024 16:39:03 +0000 Subject: [PATCH] emmas comments --- app/static/src/externalScriptSrc.ts | 19 +++++++++- app/static/src/wodin.ts | 4 +- app/static/src/wodinStatic.ts | 26 ++++++++----- app/static/src/wodinStaticUtils.ts | 37 +++++++++++++++---- .../tests/unit/externalScriptSrc.test.ts | 4 +- .../tests/unit/wodinStaticUtils.test.ts | 19 +++++++--- 6 files changed, 82 insertions(+), 27 deletions(-) diff --git a/app/static/src/externalScriptSrc.ts b/app/static/src/externalScriptSrc.ts index e5f46723..5f440e74 100644 --- a/app/static/src/externalScriptSrc.ts +++ b/app/static/src/externalScriptSrc.ts @@ -1,8 +1,25 @@ +/* + Originally dynamic wodin returned a mustache template from the express server + which had a + + + + tag in it but with the introduction of the wodin static build, we no longer + control the html that is shipped to the user (the scientists write the html + research papers with the static build). + + We could enforce that they add this script tag manually otherwise mathjax + (latex style rendering in html) won't work but for the sake of the researchers' + experience lets make it easier and less prone to errors by just programmatically + adding the script tag. + + To load more external scripts, just append the src to the externalScripts array. +*/ export const externalScripts = [ "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" ]; -export const mountScriptTags = () => { +export const loadThirdPartyCDNScripts = () => { externalScripts.forEach(src => { const script = document.createElement("script"); script.defer = true; diff --git a/app/static/src/wodin.ts b/app/static/src/wodin.ts index 470251e2..5adb0666 100644 --- a/app/static/src/wodin.ts +++ b/app/static/src/wodin.ts @@ -12,11 +12,11 @@ import help from "./directives/help"; import App from "./components/App.vue"; import { getStoreOptions } from "./wodinStaticUtils"; import { Store, StoreOptions } from "vuex"; -import { mountScriptTags } from "./externalScriptSrc"; +import { loadThirdPartyCDNScripts } from "./externalScriptSrc"; declare let appType: AppType; -mountScriptTags(); +loadThirdPartyCDNScripts(); const { Basic, Fit, Stochastic } = AppType; export const getComponent = (appType: AppType) => { diff --git a/app/static/src/wodinStatic.ts b/app/static/src/wodinStatic.ts index dc8d9610..c495d6dc 100644 --- a/app/static/src/wodinStatic.ts +++ b/app/static/src/wodinStatic.ts @@ -3,17 +3,26 @@ import { getStoreOptions } from "./wodinStaticUtils"; import { Store, StoreOptions } from "vuex"; import "./scss/style.scss" import { AppState } from "./store/appState/state"; -import { mountScriptTags } from "./externalScriptSrc"; +import { loadThirdPartyCDNScripts } from "./externalScriptSrc"; import { - componentsAndSelectors, getConfigsAndModels, getDeepCopiedStoreOptions, + componentsAndSelectors, getConfigAndModelForStores, getDeepCopiedStoreOptions, getStoresInPage, initialiseStore, waitForBlockingScripts } from "./wodinStaticUtils"; +/* + This is the entrypoint to the wodin static build. This boot function just gets called + at the end of the file. Since we are not allowed top level awaits we have to create + an async function and call it at the bottom. + + To load any scripts that you need before the runs just append the src to the + blockingScripts array. +*/ + const blockingScripts = ["./stores/runnerOde.js", "./stores/runnerDiscrete.js"]; const boot = async () => { // inject external scripts such as mathjax - mountScriptTags(); + loadThirdPartyCDNScripts(); // wait for scripts we can't load without such as runner code await waitForBlockingScripts(blockingScripts); @@ -22,12 +31,11 @@ const boot = async () => { const storesInPage = getStoresInPage(); // get relevant store configs and model code - const { configs, modelResponses } = await getConfigsAndModels(storesInPage); + const configAndModelObj = await getConfigAndModelForStores(storesInPage); - storesInPage.forEach(async (s, i) => { - const { appType, defaultCode } = configs[i]; - const modelResponse = modelResponses[i]; - const originalStoreOptions = getStoreOptions(appType) as StoreOptions; + storesInPage.forEach(async s => { + const { config, modelResponse } = configAndModelObj[s]; + const originalStoreOptions = getStoreOptions(config.appType) as StoreOptions; // recursively deep copy state because without this we would have multiple // stores writing to the same state objects @@ -35,7 +43,7 @@ const boot = async () => { // manually initialise store because this is the responsibility of apps like // WodinSession which aren't used in the static build - await initialiseStore(store, appType, defaultCode, modelResponse); + await initialiseStore(store, config, modelResponse); // mount components to dom elements based on selectors componentsAndSelectors(s).forEach(({ component, selector }) => { diff --git a/app/static/src/wodinStaticUtils.ts b/app/static/src/wodinStaticUtils.ts index 52dd55ca..53aeeb5a 100644 --- a/app/static/src/wodinStaticUtils.ts +++ b/app/static/src/wodinStaticUtils.ts @@ -61,12 +61,23 @@ export const getStoresInPage = () => { return storesInPage; }; -export const getConfigsAndModels = async (storesInPage: string[]) => { +// TODO more tolerant error handling, maybe one config didnt work (for error handling PR) +export type StaticConfig = { appType: AppType, defaultCode: string[] }; +type ConfigAndModel = { config: StaticConfig, modelResponse: OdinModelResponse }; + +export const getConfigAndModelForStores = async (storesInPage: string[]) => { const configPromises = storesInPage.map(s => axios.get(`./stores/${s}/config.json`)); const modelResponsePromises = storesInPage.map(s => axios.get(`./stores/${s}/model.json`)); - const configs = (await Promise.all(configPromises)).map(res => res.data) as { appType: AppType, defaultCode: string[] }[]; + const configs = (await Promise.all(configPromises)).map(res => res.data) as StaticConfig[]; const modelResponses = (await Promise.all(modelResponsePromises)).map(res => res.data) as OdinModelResponse[]; - return { configs, modelResponses }; + + return Object.fromEntries(storesInPage.map((s, i) => { + const cfgAndModel: ConfigAndModel = { + config: configs[i], + modelResponse: modelResponses[i] + }; + return [ s, cfgAndModel ]; + })); }; export const getDeepCopiedStoreOptions = (storeOptions: StoreOptions) => { @@ -87,14 +98,26 @@ export const getDeepCopiedStoreOptions = (storeOptions: StoreOptions) declare let odinjs: OdinRunnerOde declare let dust: OdinRunnerDiscrete +/* + Traditionally in dynamic wodin, initialising the store is the responsibility + of the components on mount (such as WodinSession initialising the app config) + however, we do not mount these components anymore in static wodin. Static wodin + may or may not mount these components so we need to guarantee that these bits + of state are initialised. + + Note: we have not thoroughly explored mounting components that impact these + specific parts of the store yet as there is no need to in static wodin. In theory + we have disabled the API service so they should not change the state but if you + are getting API related errors on static build then it returning undefined as the + response may be the cause. +*/ export const initialiseStore = async ( - store: Store, appType: AppType, - defaultCode: string[], modelResponse: OdinModelResponse + store: Store, config: StaticConfig, modelResponse: OdinModelResponse ) => { const appConfigPayload = { - appType, + appType: config.appType, basicProp: "", - defaultCode, + defaultCode: config.defaultCode, endTime: 100, readOnlyCode: true, stateUploadIntervalMillis: 2_000_000, diff --git a/app/static/tests/unit/externalScriptSrc.test.ts b/app/static/tests/unit/externalScriptSrc.test.ts index 8d707594..e87ea544 100644 --- a/app/static/tests/unit/externalScriptSrc.test.ts +++ b/app/static/tests/unit/externalScriptSrc.test.ts @@ -1,8 +1,8 @@ -import { externalScripts, mountScriptTags } from "../../src/externalScriptSrc" +import { externalScripts, loadThirdPartyCDNScripts } from "../../src/externalScriptSrc" describe("external script src", () => { test("mounts enternal scripts to document", () => { - mountScriptTags(); + loadThirdPartyCDNScripts(); externalScripts.forEach((src, i) => { const script = document.body.getElementsByTagName("script").item(i) expect(script!.src).toBe(src); diff --git a/app/static/tests/unit/wodinStaticUtils.test.ts b/app/static/tests/unit/wodinStaticUtils.test.ts index b059fd1b..e55162d9 100644 --- a/app/static/tests/unit/wodinStaticUtils.test.ts +++ b/app/static/tests/unit/wodinStaticUtils.test.ts @@ -1,5 +1,9 @@ import { AppType } from "@/store/appState/state"; -import { componentsAndSelectors, getStoreOptions, waitForBlockingScripts, getStoresInPage, getConfigsAndModels, getDeepCopiedStoreOptions, initialiseStore } from "@/wodinStaticUtils"; +import { + componentsAndSelectors, getStoreOptions, waitForBlockingScripts, + getStoresInPage, getConfigAndModelForStores, getDeepCopiedStoreOptions, + initialiseStore +} from "@/wodinStaticUtils"; import { storeOptions as basicStoreOptions } from "@/store/basic/basic"; import { storeOptions as fitStoreOptions } from "@/store/fit/fit"; import { storeOptions as stochasticStoreOptions } from "@/store/stochastic/stochastic"; @@ -68,9 +72,11 @@ describe("wodin static utils", () => { test("can get configs and models for stores", async () => { const testStores = ["store1", "store2"]; - const { configs, modelResponses } = await getConfigsAndModels(testStores); - configs.forEach((c, i) => expect(c).toBe(mockConfigRes(testStores[i]))); - modelResponses.forEach((m, i) => expect(m).toBe(mockModelRes(testStores[i]))); + const configAndModelObj = await getConfigAndModelForStores(testStores); + testStores.forEach(s => { + expect(configAndModelObj[s].config).toBe(mockConfigRes(s)); + expect(configAndModelObj[s].modelResponse).toBe(mockModelRes(s)); + }) }); test("can deep copy store options", () => { @@ -97,8 +103,9 @@ describe("wodin static utils", () => { const commit = vi.fn(); const dispatch = vi.fn(); await initialiseStore( - { commit, dispatch } as any, AppType.Basic, - ["test", "code"], "test model res" as any + { commit, dispatch } as any, + { appType: AppType.Basic, defaultCode: ["test", "code"] }, + "test model res" as any ); expect(commit).toBeCalledTimes(4);