callstack / repack

A Webpack-based toolkit to build your React Native application with full support of Webpack ecosystem.
https://re-pack.dev
MIT License
1.48k stars 110 forks source link

Local module federation implementation #782

Open AbrahamBaby opened 2 weeks ago

AbrahamBaby commented 2 weeks ago

Describe the bug

Hi devs, I was doing a POC on the repack Module Federation implementation with local MFE and could see in the superapp showcase regarding the poc-local-module-federation and could see there are some modifications needed for the mfe to work locally without any server, so i was able to see what changes needs to be done so that it will work on ios, but there were no references on how to implement it android, it would be helpful if anyone can comment on this.

Thank you

System Info

React native 0.74.4

Re.Pack Version

4.4.0

Reproduction

.

Steps to reproduce

.

zmzlois commented 2 weeks ago

can you provide a minimal reproduction so we can look into this? thank you

AbrahamBaby commented 2 weeks ago

Hi, @zmzlois, I have created 3 MFE’s and one of them i want to load from local not from server, i saw one example in the super-app-showcase where it was briefly explained how we can load a mfe using local module federation, but in that example it was mentioned how to load in ios only , there were no info regarding how to load it in android, it would be helpful if you can provide any reference for loading the local mfe in android also

Reference of super app showcase local mfe setup: https://github.com/callstack/super-app-showcase/tree/local-module-federation-poc

AbrahamBaby commented 2 weeks ago

Hi @jbroma / @thymikee / @zmzlois It would be helpful if you can provide some assistance in the above mentioned issue, was running the local-module-federation-poc from super-app-showcase and was stuck when running it for android. Please provide a response as i am unable to move forward with the implementation.

Thanks in advance

slubal commented 1 week ago

For what its worth, I added my own scrappy script cache by having script load interceptors like following This enabled following functionality in our app,

  1. Download the script from whatever URL
  2. intercept the loadScript call to update the URL from http.... to file:///
  3. If the http or anything is down for whatever reason fallback to last synced file
  4. If the fallback is not present then fallback to baked in split bundles ( this will require you to have a map that you can fallback to but it works by simply providing location of your split bundle)

In your main app have following

function makeSafeFilename(url: string): string { // Extract the part of the URL after the domain name (strip the protocol and domain) const urlPath = url.replace(/^https?:\/\/[^/]+/, '');

// Replace slashes (/) with dashes (-) and append a .js extension
return urlPath.replace(/\//g, '-') + '.js';

}

export const ScriptInterceptor = async function (script: Script, shouldUseCache = true) { const cacheDir = ${RNFS.DocumentDirectoryPath}/script_cache;

// Construct the full URL with query parameters
const url = script.locator.query
    ? `${script.locator.url}?${script.locator.query}`
    : script.locator.url;

// Use the full URL as part of the cache key to differentiate scripts with different queries
const cacheKey = script.scriptId;//makeSafeFilename(url);
const cacheFilePath = `${cacheDir}/${cacheKey}.js`;

// Ensure cache directory exists
try {
    if (!(await RNFS.exists(cacheDir))) {
        await RNFS.mkdir(cacheDir);
    }
} catch (dirError: any) {
    console.error(`Failed to create cache directory: ${dirError.message}`);
    throw dirError;
}

const assignLocatorUrlFromLocalFileSystem = (fileLocation: string, isAbsolutePath = true) => {
    // assign script to have absolute path
    script.locator.absolute = true;
    script.locator.url = fileLocation;
}

// Check if the script is already cached
if (await RNFS.exists(cacheFilePath) && shouldUseCache) {
    console.log(`Cache hit for script: ${cacheFilePath}`);
    assignLocatorUrlFromLocalFileSystem(`file://${cacheFilePath}`);
    console.log('updated path', script.locator.url)
    return;
}

console.log(`Downloading script: ${url}`);
try {
    const response = await fetch(url, {
        method: script.locator.method,
        headers: script.locator.headers,
        body: script.locator.body,
    });

    if (!response.ok) {
        console.log('error downloading script', response.status, 'for script id:', script.scriptId);
        return;
    }

    const scriptContent = await response.text();

    // Cache the script to the filesystem
    await RNFS.writeFile(cacheFilePath, scriptContent, 'utf8');
    console.log(`Cached script at: ${cacheFilePath}`);

    // Update locator to use the file URL
    assignLocatorUrlFromLocalFileSystem(`file://${cacheFilePath}`);
} catch (error: any) {
    console.error(`Error in interceptor while downloading script: ${error.message}`);
    return;
    //throw error;
}

};


You do need to patch the ScriptManager.js directly or use patch-package library 

diff --git a/node_modules/@callstack/repack/dist/modules/ScriptManager/ScriptManager.js b/node_modules/@callstack/repack/dist/modules/ScriptManager/ScriptManager.js index ff3dc81..824b607 100644 --- a/node_modules/@callstack/repack/dist/modules/ScriptManager/ScriptManager.js +++ b/node_modules/@callstack/repack/dist/modules/ScriptManager/ScriptManager.js @@ -256,7 +256,6 @@ export class ScriptManager extends EventEmitter { }

     this.emit('resolved', script.toObject()); // if it returns false, we don't need to fetch the script

- return script; } // If no custom shouldUpdateScript function was provided, we use the default behaviour

@@ -309,6 +308,10 @@ export class ScriptManager extends EventEmitter { try { this.emit('loading', script.toObject()); this.on('loaded', onLoaded);