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

mrc-6023 POC static build #236

Open
wants to merge 1 commit into
base: mrc-6022
Choose a base branch
from
Open

mrc-6023 POC static build #236

wants to merge 1 commit into from

Conversation

M-Kusumgar
Copy link
Collaborator

@M-Kusumgar M-Kusumgar commented Nov 17, 2024

ignore the merge conflicts and test failures! just a POC

Testing

  1. make sure you are on node 20 (wodin is now upgraded)
  2. run npm ci --prefix=app/static and npm ci --prefix=app/server to get all updated packages
  3. this approach required the odin.api container so run ./scripts/run-dev-dependencies.sh (dont need redis but just using the normal script for now)
  4. now youre ready! run ./scripts/build-and-serve-static-site.sh and have fun, itll be on localhost:3000

General approach

Network requests

We want to decouple the frontend from the express backend, we can just ignore all the sessions stuff in the app as far as I could tell which is the majority of the work. This leaves the initial setup which comprises of getting:

  • config
  • compiled code
  • ode and discrete runners
  • versions

everything except the config is fetched from odin.api (for now treating the code in "defaultCode" folder as the static code) so we can do exactly that, we can prefetch from the container that we start up on the users machine these responses and just save them as a json file that can be shipped with the static site. these responses can now be fetched with the overrides in the apiService.ts file (_overrideGetRequestsStaticBuild and _overridePostRequestsStaticBuild methods)

as for the config, we can reuse the handy ConfigController to inject the config with the defaults (exactly in the same way the server does) with a tiny change, we always keep readOnlyCode as true since this is our static build and we save this as a static json file too same as other requests

we simply ignore all other requests the app makes

Frontend

The frontend takes in an env arg VITE_STATIC_BUILD to switch between the two build types, main differences you may see are things like not checking session before initialising the app, no sessions dropdown, no "Reset" or "Compile" code buttons, the default tab to start with for users being "Options" instead of the "Code" tab because the code doesnt change

How does this translate to users workflow?

We publish a static wodin builder package that includes:

  • static wodin js and css files
  • wodinBuilder javascript code (we just tsc with entrypoint as wodinBuilder.ts its only a couple of tiny files from the app/server)
  • our site views (ripped out "handlebars" actually, now they are "mustache" files as handlebars is an extension to mustache but we dont need any of the extension features at all, mustache dependency is 114kb unpacked where as handlebars is over 2mb unpacked so thought why not, its almost the same syntax)

The user basically creates the same config as before for a wodin site (perhaps we can change "defaultCode" folder to just "code" folder but perhaps not since users will already have wodin configs ready and can just reuse existing ones if they wish)

wodinBuilder script takes in config path and destination path as args (views path arg is just for my convenience when i was testing) and so the user can tell us where the config lies and where their public folder is. we can also exec some docker commands from that script to run the odin.api container on their machines, and then the builder has everything it needs to generate out the whole wodin website

Some final thoughts

The outlined approach above requires the user to have both docker and node installed on their machines. Some other approaches we can consider:

  • we could do everything described above in a docker container itself and that would mean that the user only needs docker installed on their computers, this might be a bit easier on the user at the cost of a bigger image they have to download
  • one way that i am beginning to like more and more is that the contents of the package so the wodin js, css, wodin builder files can be instead part of a git repo that they clone down (depth 1 so dw), with some scripts we save as part of that git repo that help users with general setup, they can run simple bash scripts that validate if they have docker and node for example and redirect them to the correct resources if not, etc, people in the department already have git (most of them) and this means that they dont have to actually install node to even access the package and the download remains small, they dont even have to copy the config over to the repo, just need to give the wodinBuilder the correct input and output

Copy link

codecov bot commented Nov 17, 2024

Codecov Report

Attention: Patch coverage is 53.48837% with 20 lines in your changes missing coverage. Please review.

Project coverage is 99.29%. Comparing base (748c80f) to head (6a4a090).
Report is 2 commits behind head on mrc-6022.

Files with missing lines Patch % Lines
app/static/src/apiService.ts 10.00% 11 Missing and 7 partials ⚠️
app/static/src/components/WodinSession.vue 90.00% 1 Missing ⚠️
app/static/src/router.ts 50.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           mrc-6022     #236      +/-   ##
============================================
- Coverage     99.78%   99.29%   -0.49%     
============================================
  Files           159      160       +1     
  Lines          4107     4137      +30     
  Branches        936      955      +19     
============================================
+ Hits           4098     4108      +10     
- Misses            8       21      +13     
- Partials          1        8       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@M-Kusumgar M-Kusumgar changed the title poc done mrc-6023 POC static build Nov 17, 2024
@EmmaLRussell
Copy link
Contributor

we could do everything described above in a docker container itself and that would mean that the user only needs docker installed on their computers, this might be a bit easier on the user at the cost of a bigger image they have to download

Wasn't there talk of doing this in a github action at one point? So user wouldn't actually need to build on their local machine. And could then publish direct to ghp potentially. But maybe I'm misremembering.

@M-Kusumgar
Copy link
Collaborator Author

Wasn't there talk of doing this in a github action at one point? So user wouldn't actually need to build on their local machine. And could then publish direct to ghp potentially. But maybe I'm misremembering.

yep i definitely forgot a bit about that, that just solves the whole issue, we can just setup the env however we want in our custom action

Copy link
Contributor

@EmmaLRussell EmmaLRussell left a comment

Choose a reason for hiding this comment

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

This is very cool! Just a few thoughts on keeping everything tidy.

@@ -23,7 +23,7 @@ export const configDefaults = (appType: string) => {
};

export class ConfigController {
private static _readAppConfigFile = (
static readAppConfigFile = (
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess we'll pull this out of the controller when we implement for real.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

actually we dont have to! we need the whole config controller any and tree shaking means that we only get that and not everything else, we just put wodinBuilder as the entry point

@@ -0,0 +1,15 @@
const doc = `
Usage:
builder <path-to-config> <dest-path> <path-to-mustache-views>
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah. the views path doesn't seem like it needs to be a parameter as it's not something that should change per build..?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

it can change between development and production, it depends on the folder structure of the dist folder, which may not be the same as the folder structure of our app/server directory, i guess we can also force them to be the same and hardcode that path in wodin builder as well but felt nice to give that flexibility

appTitle: config.title,
courseTitle: wodinConfig.courseTitle,
wodinVersion,
loadSessionId: sessionId || "",
Copy link
Contributor

Choose a reason for hiding this comment

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

This should always be null, right? Same for shareNotFound.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ooo yes, completely forgot to just change those to null!

errors: null,
data: readOnlyConfigWithDefaults
};
fs.writeFileSync(path.resolve(appNamePath, "config.json"), JSON.stringify(configResponse));
Copy link
Contributor

Choose a reason for hiding this comment

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

So it's writing out a pre-canned response file, status and all, for config etc, which will be read by the front end rather than talking to the server? I'd assumed that the apiService in the front end would, when configured as static, just read this data directly from the public path of the site, but I guess it makes the code simpler if it assumes that all these "responses" are going to be in the same format as for the dynamic site. I see you're doing a similar thing for the version and runner responses, just piping the the response direct to file.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yep exactly i went back and forth in terms of how to get these responses, also considered them just being javascript files or something like that, all the other options just required a bit more code change which isnt a problem but this seemed the neatest to me, its literally just a fake local api but yh all logic works exactly the same, we dont need to do any extra processing of the response or anything like that

i feel like the more "branches" we add with these two modes diverging the more maintenance we add

fs.writeFileSync(path.resolve(appNamePath, "config.json"), JSON.stringify(configResponse));


const versionsResponse = await axios.get("http://localhost:8001/");
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess for the full implementation we'll make the api path configurable, deal with error handling etc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes definitely!



const versionsResponse = await axios.get("http://localhost:8001/");
fs.writeFileSync(path.resolve(appNamePath, "versions.json"), JSON.stringify(versionsResponse.data));
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we could make the mappings between the api paths and the file names a bit less arbitrary. So if the path to the json file was always the same as the url in the backend that is called in the dynamic app e.g. /runner/ode.json. But then it's complicated by being scoped by app.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

sure yh i dont think having folders per app is that bad personally if we want to keep them consistent with the api urls

Comment on lines +61 to +67
const runnerResponse = await axios.get("http://localhost:8001/support/runner-ode");
fs.writeFileSync(path.resolve(appNamePath, "runnerOde.json"), JSON.stringify(runnerResponse.data));

if (configWithDefaults.appType === "stochastic") {
const runnerResponse = await axios.get("http://localhost:8001/support/runner-discrete");
fs.writeFileSync(path.resolve(appNamePath, "runnerDiscrete.json"), JSON.stringify(runnerResponse.data));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

These responses are identical for every model aren't they? so I guess they don't need to be fetched or even saved per app?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

they are! the reason i did it per app is the request fired is relative to the app, so like apps/day1/runner/ode or something like that, so it was easier to have a duplicate in each app, obviously this doesnt scale well but i dont think people ever have more than 5-6 apps, so that much duplication of that file seemed alright for slightly simpler code but happy to change it if needed

@@ -1,7 +1,7 @@
<template>
<div>
<button
v-if="defaultCodeExists"
v-if="defaultCodeExists && !STATIC_BUILD"
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like it would maybe be a bit nicer if the components themselves didn't know directly about STATIC_BUILD and read a getter from the store to say if code should be resettable etc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

i can see that in this case perhaps, but then places like WodinSession.vue we have code in a watcher that relies on the static build variable, in BasicApp.vue and other app types we have a switch on whether "Options" is the tab shown initially or not, and in some places we just have a v-if on only the static build variable

so perhaps in the app types we can have shared code, either a getter or a function that tells us what the initial tab is, but for a v-if on just the static build variable im not sure a getter or a function does anything for us, itll just wrap static build in another layer

yh im just not sure how we have getters that cover all these slightly different ways of using static build without basically creating a different getter for each of these use cases, at which point we may as well use it in the component

Copy link
Contributor

Choose a reason for hiding this comment

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

yeah, fair point. But as you say for the lower level components like this one, maybe it should just be a prop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants