Skip to content

(metro-core): add manifest SHA-256 hashes and optional bundle cache layer integration#4576

Draft
zhongwuzw wants to merge 25 commits intomodule-federation:mainfrom
zhongwuzw:features/metro-cache
Draft

(metro-core): add manifest SHA-256 hashes and optional bundle cache layer integration#4576
zhongwuzw wants to merge 25 commits intomodule-federation:mainfrom
zhongwuzw:features/metro-cache

Conversation

@zhongwuzw
Copy link
Copy Markdown

@zhongwuzw zhongwuzw commented Mar 20, 2026

Description

  • Adds SHA-256 bundle hashes to federation manifest during bundle-remote build (metaData.buildInfo.hash, exposes[].hash, shared[].hash).
  • Introduces an optional runtime cache integration via global MFE_CACHE_LAYER (new ICacheLayer contract).
  • asyncRequire now routes remote bundle loading through the cache layer when enabled (production by default), with inflight dedup and safe fallback to loadBundleAsync on skipped.
  • metroCorePlugin.afterResolve extracts resolved bundle URLs (dev/prod compatible) from manifest and registers expected hashes + manifest source to the cache layer (best-effort).

Related Issue

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Checklist

  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • I have updated the documentation.

@netlify
Copy link
Copy Markdown

netlify bot commented Mar 20, 2026

Deploy Preview for module-federation-docs ready!

Name Link
🔨 Latest commit b247277
🔍 Latest deploy log https://app.netlify.com/projects/module-federation-docs/deploys/69c54f47d92ce80009503f85
😎 Deploy Preview https://deploy-preview-4576--module-federation-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 20, 2026

⚠️ No Changeset found

Latest commit: b247277

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@zhongwuzw zhongwuzw changed the title [metro-cache] metro-cache preview [metro-core] metro-core add cache Mar 26, 2026
@zhongwuzw zhongwuzw changed the title [metro-core] metro-core add cache (metro-core): add manifest SHA-256 hashes and optional bundle cache layer integration Mar 26, 2026
Copy link
Copy Markdown
Member

@jbroma jbroma left a comment

Choose a reason for hiding this comment

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

hey @zhongwuzw , nice job with the implementation so far, I have few questions & one ideas about how to make this even better, let me know what you think!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

since there are no package.json changes in this PR, this is probably a leftover from previous changes, could you please verify this?

Comment on lines +375 to +378
const bundleContent = await fs.readFile(
saveBundleOpts.bundleOutput,
'utf-8',
);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

perhaps we can use bundle directly here? I don't see any benefit of reading it again from the filesystem

Copy link
Copy Markdown
Author

@zhongwuzw zhongwuzw Apr 1, 2026

Choose a reason for hiding this comment

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

@jbroma Hi, I see we have --bundle-encoding in bundle-mf-remote — do we actually support non-UTF-8? https://github.com/zhongwuzw/core/blob/b2472772250dad91947eefa31b724582441f95e9/packages/metro-core/src/commands/bundle-remote/index.ts#L335

The option accepts utf8 | utf16le | ascii, but encoding info is never passed to the upload layer or stored in the manifest. The server/CDN has no idea what encoding the bundle was written in, and the client always decodes as UTF-8? If a user passes --bundle-encoding utf16le, the bundle will silently break at runtime I think. Did I miss anything?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Probably not, it's fine to assume utf-8 for now - ideally we would store this information somewhere so we dont have to guess

// Inject container bundle hash into metaData.buildInfo.hash
const containerFilename = federationConfig.filename;
if (bundleHashMap.has(containerFilename)) {
rawManifest.metaData.buildInfo.hash =
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is this typed in the MF core or is this a non-standard field?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

shared[].hash is already typed in MF core (StatsShared.hash: string) — Metro generates it as an empty string and I populate it post-build. metaData.buildInfo.hash and expose[].hash are non-standard — StatsBuildInfo and StatsExpose don't have a hash field. Happy to add hash?: string to the core types if you'd like to formalize it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yes, let's standardize those then 👍

Comment on lines +121 to +157
afterResolve: (args) => {
// Register bundle hashes with cache layer for integrity verification
try {
const cacheLayer = (globalThis as any).__MFE_CACHE_LAYER__ as
| ICacheLayer
| undefined;
if (!cacheLayer) return args;

const __loadBundleAsync =
globalThis[`${__METRO_GLOBAL_PREFIX__ ?? ''}__loadBundleAsync`];
const { origin, remoteInfo, remote } = args;
const manifestUrl =
'entry' in remote ? (remote as any).entry : undefined;
if (manifestUrl && origin.snapshotHandler?.manifestCache) {
const manifest =
origin.snapshotHandler.manifestCache.get(manifestUrl);
if (manifest) {
// Container bundle hash
const containerHash = (manifest.metaData?.buildInfo as any)?.hash;
if (containerHash && remoteInfo.entry) {
cacheLayer.registerBundleHash(remoteInfo.entry, containerHash);
}

const loadBundleAsync =
__loadBundleAsync as typeof globalThis.__loadBundleAsync;
// Exposed + shared bundle hashes
const hashes = extractBundleHashes(manifest, manifestUrl);
for (const [url, hash] of hashes) {
// Strip query params — loadBundle looks up hashes by bare URL
cacheLayer.registerBundleHash(url.split('?')[0], hash);
}

if (!loadBundleAsync) {
throw new Error('loadBundleAsync is not defined');
}
// Register manifest source for polling
cacheLayer.registerManifestSource(manifestUrl, extractBundleHashes);
}
}
} catch {
// non-critical — hash validation is best-effort
}
return args;
},
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we could extract this to the cache package and make it a separate runtime plugin instead - do you think it's possible?

Comment on lines +102 to +123
// For remote split bundles with cache enabled, convert relative paths to
// full URLs so they enter the same cache path as container bundles.
// In dev mode, getBundlePath returns relative paths unchanged, but we need
// full URLs for the cache layer (download + eval).
if (isSplitBundle && cacheLayer && publicPath && !isUrl(bundlePath)) {
bundlePath = joinComponents(publicPath, bundlePath);
}

// --- Cache layer: intercept bundles with full URLs (containers + remote split bundles) ---
if (cacheLayer && isUrl(bundlePath)) {
const { status } = await cacheLayer.loadBundle(bundlePath);
if (status === 'skipped') {
// Cache layer skipped — fall back to network load
const encodedBundlePath = bundlePath.replaceAll('../', '..%2F');
await loadBundleAsync(encodedBundlePath);
}
// else: 'cache-hit' or 'downloaded' — bundle already eval'd by cache layer
} else {
// No cache: host split bundles (no publicPath), cache disabled, or native-cache not installed
const encodedBundlePath = bundlePath.replaceAll('../', '..%2F');
await loadBundleAsync(encodedBundlePath);
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good but to me it feels out of place - with an optional layer we are introducing a lot of complexity into this module - do you think it would be possible to create wrapper around loadBundleAsync that introduces the cache layer and then load the MF wrapper?

I think we could rework the asyncRequire implementation, so that it would be possible to modify the actual loadBundleAsync and then add MF wrapper on top of it:

  1. InitializeCore runs -> __loadBundleAsync gets initialized with default implementation
  2. Cache wrapper runs -> enhances __loadBundleAsync with caching capabilities
  3. MF metro-core wrapper runs -> adapts __loadBundleAsync to work with MF

This approach would most likely require splitting asyncRequire into two modules, something like this:

  • mf:init-async-require -> injects expo impl of async require if missing
  • mf:adapt-async-require -> adapts existing impl of async require to work with federation

in the cache plugin you could then modify the resolver for either of those to ensure module with cache wrapper runs after init and before adapt

mf:async-require, left over for compatibility could be then just:

import `mf:init-async-require`;
import `mf:adapt-async-require`;

let me know what you think & if that makes sense to you

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