Skip to content

Conversation

@hinakhadim
Copy link
Contributor

@hinakhadim hinakhadim commented Oct 31, 2024

WIP -> The jobs run on tutor dev only

Description

These code changes introduce improvements to the Tutor MFE (Micro-Frontend) plugin by adding automation and efficiency for managing and running tasks in environments where MFEs are mounted. When MFEs are mounted, it will run npm clean-install as part of the run tutor dev launch. The "mfe" init task npm-install.sh is taking care of installing the node_modules, as it is done in the LMS

Test Cases:

  • It will run the npm-install script when MFEs are implicitly and expicitly mounted using the Tutor format: tutor mounts add $(pwd)/frontend-app-* & tutor mounts add [mfe-name]:$(pwd)/frontend-app-*:/openedx/app.
  • The job should run for all mounted MFEs
  • The job will run only in tutor dev, not tutor local, as MFEs mount only in tutor dev environment.
  • If no node_modules are present, it should install node_modules.
  • If the node_modules directory exists but is empty or outdated, it should install node_modules.
  • If the package-lock.json file changes slightly, it should trigger reinstallation of node_modules.
  • If node_modules are installed and then reinstallation is attempted without changes to package-lock.json, it should not reinstall node_modules.

Closes #218

Special thanks to @Danyal-Faheem for collaborating with me on this and helping to reach a solution. 🙌

@hinakhadim hinakhadim added the bug Something isn't working label Oct 31, 2024
@hinakhadim hinakhadim marked this pull request as draft October 31, 2024 11:48
@hinakhadim hinakhadim changed the title fix: add script to install node_modules in mounted mfes [WIP] fix: add script to install node_modules in mounted mfes Oct 31, 2024
def _run_jobs_in_mounted_mfes(config: Config) -> None:
mounts = get_typed(config, "MOUNTS", list, [])

pattern = re.compile(r'frontend-app-(\w+)')
Copy link
Contributor

Choose a reason for hiding this comment

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

We can use the REPO_PREFIX variable defined above here instead of a hardcoded string.

@tutor_hooks.Actions.CONFIG_LOADED.add()
def _run_jobs_in_mounted_mfes(config: Config) -> None:
mounts = get_typed(config, "MOUNTS", list, [])

Copy link
Contributor

Choose a reason for hiding this comment

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

We can return here if no mounts exist in the config.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

@@ -0,0 +1,11 @@
{%- for app_name, app in iter_mfes() %}
Copy link
Contributor

Choose a reason for hiding this comment

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

why is this patch needed?

Copy link
Contributor

Choose a reason for hiding this comment

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

This patch is required to add the necessary jobs to run the init jobs inside the containers of the MFEs.

pattern = re.compile(r'frontend-app-(\w+)')
mounted_mfes = [match.group(1) for mount in mounts if (match := pattern.search(mount))]

mfe_task_file = os.path.join(
Copy link
Contributor

Choose a reason for hiding this comment

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

mfe_npm_install could be a better name as this action is related only to npm install.

sha1sum "$PACKAGE_LOCK" | awk '{print $1}'
}

# Check if node_modules exists and fingerprint is valid
Copy link
Contributor

Choose a reason for hiding this comment

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

this comment can be broken down and mentioned against the appropriate if-elif-else block.



@tutor_hooks.Actions.CONFIG_LOADED.add()
def _run_jobs_in_mounted_mfes(config: Config) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not a big fan of adding callbacks to the CONFIG_LOADED action. It means we have to manually parse MOUNTS and this is bound to fail in many edge cases. Also, we have to start using Context objects, which is VERY dirty.

Instead, I suggest the following:

@tutor_hooks.Filters.CLI_DO_INIT_TASKS.add()
def _mfe_npm_install(tasks):
    for mfe in get_mfes():
        if hooks.Filters.COMPOSE_MOUNTS.apply([], f"frontend-app-{mfe}"):
            tasks.append((mfe, "... contents of npm-install.sh..."))

What do you think?

Copy link
Collaborator

@arbrandes arbrandes Sep 19, 2025

Choose a reason for hiding this comment

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

At first glance I agree with @regisb, here. If there are no technical blockers to doing it this way, I think it would be less problematic than adding a -jobs Docker service that needs to be duplicated in both dev and local compose files.

We're already assuming frontend-app-* in REPO_PREFIX anyway, and using it in different mount-related places. No harm in re-using it.

@HammadYousaf01 HammadYousaf01 moved this from In Progress to In review in Tutor project management May 6, 2025
@Danyal-Faheem Danyal-Faheem self-requested a review May 13, 2025 11:45

# Store the new fingerprint after installation
generate_fingerprint > "$FINGERPRINT_FILE"
echo "✅ Dependencies installed and fingerprint updated." No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: add extra line at end of file

- {{ mount }}
{%- endfor %}
{%- endif %}
{%- endfor %} No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: add extra line at end of file

@@ -0,0 +1,11 @@
{%- for app_name, app in iter_mfes() %}
{%- set mounts = iter_mounts(MOUNTS, app_name)|list %}
Copy link
Contributor

Choose a reason for hiding this comment

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

We moved from iter_mounts to a cleaner MFEMountData object in #241. We should use the similar format here for consistency.

@@ -0,0 +1,7 @@
{%- for app_name, app in iter_mfes() %}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this patch necessary? Don't we just need to run the init job in dev?

return None

pattern = re.compile(rf'{re.escape(REPO_PREFIX)}(\w+)')
mounted_mfes = [match.group(1) for mount in mounts if (match := pattern.search(mount))]
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking back, we should be able to use MFEMountData here as well instead of using a regex. What do you think?

@Danyal-Faheem Danyal-Faheem force-pushed the fix/mounted-mfes-npm-install branch from 49bb54f to 8b819e6 Compare June 18, 2025 14:15
@DawoudSheraz DawoudSheraz marked this pull request as ready for review June 24, 2025 09:08
Comment on lines +1 to +14
# We only need to define these for local as well otherwise docker throws a not found error when we run the local init jobs
# These jobs are required to run the npm install script when mounting MFEs
{%- set mfe_data = MFEMountData(MOUNTS) %}

{%- for app_name, app, mounts in mfe_data.mounted %}
{{ app_name }}-job:
image: "{{ MFE_DOCKER_IMAGE_DEV_PREFIX }}-{{ app_name }}-dev:{{ MFE_VERSION }}"
volumes:
{%- for mount in mounts %}
- {{ mount }}
{%- endfor %}
environment:
- "NODE_ENV=production"
{%- endfor %}
Copy link
Contributor

Choose a reason for hiding this comment

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

I dont understand why this is needed. It is using dev prefixes in local/production env, which in itself is not coherent. Why is local init job failing if this is not provided?

Copy link
Contributor

Choose a reason for hiding this comment

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

We add a job for every mounted MFE to the CLI_DO_INIT_TASKS filter. When we run the initialization task with the local context, aka tutor local do init, it will try to run those jobs that we added. However, we never defined a docker compose service to run that job in, therefore it fails.

We define this job structure here to let docker know where to run the job for each MFE. The job will still run in local mode but will exit due to the NODE_ENV environment variable being set to production.

The reason we added dev prefixes for local is just for consistency as we need to task to exit after discovering that NODE_ENV is set to production.

Copy link
Contributor

Choose a reason for hiding this comment

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

We add a job for every mounted MFE to the CLI_DO_INIT_TASKS filter. When we run the initialization task with the local context, aka tutor local do init, it will try to run those jobs that we added. However, we never defined a docker compose service to run that job in, therefore it fails.

Let's only do this for dev context. It is confusing to have this for local when it is not meant to be used.

Copy link
Contributor

Choose a reason for hiding this comment

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

Right, we tried to find a way to do that, however we are using the CONFIG_LOADED action to parse mounts and add our jobs. This action does not provide us any information if the job is being run with the local or dev context and therefore, we can't add it conditionally.

The only hook that provides this information is the APP_PUBLIC_HOSTS filter and we cannot use it as it doesn't provide us information regarding mounts. This is why we had no choice but to run the job in both local and dev containers and then exit if a production ENV is discovered.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, ok.

Copy link
Contributor

Choose a reason for hiding this comment

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

@arbrandes what do you think about this?

Copy link
Contributor

Choose a reason for hiding this comment

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

@arbrandes Ping 🙂

Copy link
Collaborator

Choose a reason for hiding this comment

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

Have you given @regisb's suggestion a try? I too feel that having this in the local file is troublesome, and doing it some other way (i.e., a DO_INIT hook that iterates over COMPOSE_MOUNTS) looks like it would not require adding a -jobs service.

@ahmed-arb ahmed-arb moved this from In review to Blocked in Tutor project management Jul 9, 2025
Copy link
Collaborator

@arbrandes arbrandes left a comment

Choose a reason for hiding this comment

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

Before moving forward, I'd like to understand if a less invasive alternative (@regisb's suggestion, specifically) was investigated.

Comment on lines +1 to +14
# We only need to define these for local as well otherwise docker throws a not found error when we run the local init jobs
# These jobs are required to run the npm install script when mounting MFEs
{%- set mfe_data = MFEMountData(MOUNTS) %}

{%- for app_name, app, mounts in mfe_data.mounted %}
{{ app_name }}-job:
image: "{{ MFE_DOCKER_IMAGE_DEV_PREFIX }}-{{ app_name }}-dev:{{ MFE_VERSION }}"
volumes:
{%- for mount in mounts %}
- {{ mount }}
{%- endfor %}
environment:
- "NODE_ENV=production"
{%- endfor %}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Have you given @regisb's suggestion a try? I too feel that having this in the local file is troublesome, and doing it some other way (i.e., a DO_INIT hook that iterates over COMPOSE_MOUNTS) looks like it would not require adding a -jobs service.



@tutor_hooks.Actions.CONFIG_LOADED.add()
def _run_jobs_in_mounted_mfes(config: Config) -> None:
Copy link
Collaborator

@arbrandes arbrandes Sep 19, 2025

Choose a reason for hiding this comment

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

At first glance I agree with @regisb, here. If there are no technical blockers to doing it this way, I think it would be less problematic than adding a -jobs Docker service that needs to be duplicated in both dev and local compose files.

We're already assuming frontend-app-* in REPO_PREFIX anyway, and using it in different mount-related places. No harm in re-using it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

Development

Successfully merging this pull request may close these issues.

Mounted MFEs with node_modules missing fail to start the server

6 participants