
Mf
- 382 installs
- 4 repo stars
- Updated June 23, 2026
- module-federation/agent-skills
Design Module Federation remotes and hosts for micro-frontends where teams ship independent UI bundles consumed at runtime without rebuilding the shell app.
About
Covers Module Federation patterns for agent-assisted frontend work: configuring hosts and remotes, shared module scopes, version alignment, lazy remote loading, and pitfalls when splitting SaaS UIs into independently shipped bundles.
- Host and remote contracts
- Shared dependency strategy
- Runtime remote loading
- Independent team deploys
- Rspack/webpack federation
Mf by the numbers
- 382 all-time installs (skills.sh)
- +34 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #669 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/module-federation/agent-skills --skill mfAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 382 |
|---|---|
| repo stars | ★ 4 |
| Last updated | June 23, 2026 |
| Repository | module-federation/agent-skills ↗ |
What it does
Design Module Federation remotes and hosts for micro-frontends where teams ship independent UI bundles consumed at runtime without rebuilding the shell app.
Files
MF — Module Federation All-in-One Skill
Step 1: Identify the sub-skill
Parse $ARGUMENTS and map to a reference file in the reference/ directory (same directory as this file):
| Sub-command (case-insensitive) | Aliases | Reference file |
|---|---|---|
docs | doc, help, ? | reference/docs.md |
context | ctx, info, status | reference/context.md |
module-info | module, remote, manifest | reference/module-info.md |
integrate | init, setup, add | reference/integrate.md |
type-check | types, ts, dts | reference/type-check.md |
shared-deps | shared, deps, singleton | reference/shared-deps.md |
perf | performance, hmr, speed | reference/perf.md |
config-check | config, plugin, exposes | reference/config-check.md |
bridge-check | bridge, sub-app | reference/bridge-check.md |
runtime-error | runtime-code, runtime-008, runtime-001, remote-entry | reference/runtime-error.md |
observability | obs, observe, trace, traceId, report, observability, debug-loading, telemetry, runtime-007, moduleInfo, snapshot | reference/observability.md |
If no explicit sub-command is found, detect intent from the full input:
If the input contains an observability report, traceId, console read: command, .mf/observability file path, or asks how to observe, debug, trace, inspect, or upload Module Federation loading data, or uses obs as shorthand for observability, choose reference/observability.md even when the same input also contains a RUNTIME-xxx code.
| Signal in input | Reference file |
|---|---|
| Question about MF concepts, API, configuration options | reference/docs.md |
| "integrate", "add MF", "setup", "scaffold", "new project" | reference/integrate.md |
| "type error", "TS error", "@mf-types", "dts", "typescript" | reference/type-check.md |
| "shared", "singleton", "duplicate", "antd", "transformImport" | reference/shared-deps.md |
| "slow", "HMR", "performance", "build speed", "ts-go" | reference/perf.md |
| "plugin", "asyncStartup", "exposes key", "config" | reference/config-check.md |
| "bridge", "sub-app", "export-app", "createRemoteAppComponent" | reference/bridge-check.md |
| "RUNTIME-001", "RUNTIME-008", "runtime error code", "remote entry load failed", "ScriptNetworkError", "ScriptExecutionError", "container missing", "window[remoteEntryKey]" | reference/runtime-error.md |
| "obs", "mf obs", "Observability report generated", "console.error", "traceId", "read:", "diagnosis", "ownerHint", "summary.phases", ".mf/observability", "build-report.json", "latest.json", "RUNTIME-007", "moduleInfo", "remote snapshot", "global snapshot", "snapshot match", "observability", "observe MF", "debug MF loading", "trace loading", "loading report", "open page and inspect MF", "visit URL and observe MF", "看下 MF 加载情况", "telemetry", "onReport", "onEvent", "production report", "upload observability" | reference/observability.md |
| "manifest", "remoteEntry URL", "module info", "publicPath" | reference/module-info.md |
| "context", "what is configured", "MF role", "bundler" | reference/context.md |
If still ambiguous, show the user the sub-command table above and ask them to pick.
Step 2: Load and execute the reference
Read the matched file from the reference/ directory (same directory as this SKILL.md).
Execute all instructions in that file, passing the remaining arguments (everything after the sub-command token, or the full $ARGUMENTS if intent-detected) as ARGS.
Sub-skill: bridge-check
Check Module Federation Bridge usage: verify that producers correctly export export-app, and that consumers use the recommended Bridge API.
Step 1: Collect MFContext
Read and follow the instructions in ./context.md, passing ARGS as the project root.
Step 2: Run bridge check script
Serialize MFContext to JSON and pass it to the check script:
node scripts/bridge-check.js --context '<MFContext-JSON>'Process each item in the output results and context.mfConfig:
BRIDGE-USAGE · info — No export-app export found
- No key matching the
export-apppattern found inexposes - If this project is a sub-app that should follow the Bridge spec, guide the user to:
1. Add "./export-app": "./src/export-app.tsx" to exposes 2. The exported module must return an object conforming to the Bridge spec (containing render and destroy methods)
BRIDGE-USAGE · info — Consumer API recommendation
- Advise consumers to use official Bridge APIs such as
createRemoteAppComponent - Avoid directly concatenating remote URLs or manually calling
loadRemote
If context.mfRole is host (no exposes), skip the producer-side check and only provide consumer-side recommendations.
Long-Chain Capture
Keep a tab alive across multiple steps — navigate, click through interactions, then capture.
Usage
# Step 1 — open tab, keep it alive
TAB=$(node ../scripts/browser-capture.mjs "https://example.com" --keep-tab | jq -r .tabId)
# Step 2 — click through the interaction chain (faster: domcontentloaded/none)
node ../scripts/browser-capture.mjs --tab-id "$TAB" --click "Profile" --action-wait domcontentloaded
node ../scripts/browser-capture.mjs --tab-id "$TAB" --click "Favorites" --action-wait none
# Step 3 — final action, capture variables, close tab
node ../scripts/browser-capture.mjs --tab-id "$TAB" --click "Add" --vars __FEDERATION__ --action-wait networkidle --closeFlags
| Flag | Description |
|---|---|
--keep-tab | Don't close tab after capture; outputs tabId in result |
--tab-id <id> | Attach to existing tab instead of navigating |
--click "<text or selector>" | Click an element; matching prefers CSS/interactive elements first |
--fill "placeholder::text" | Type into an input/textarea located by placeholder |
--select "placeholder::value" | Choose an option in a select located by placeholder |
| `--action-wait <auto | networkidle |
--no-entries | Exclude entries logs to speed up capture and reduce output size |
--dump-dom | Output page DOM structure (for identifying selectors) |
--close | Close the tab after this step |
Click matching
Applied in order: 1. If query starts with #, ., [, or contains > → CSS selector 2. Strong interactive elements (button, a, role/button/tab/menuitem/option, submit/button inputs) 3. Weak interactive elements (div/span/li) only when they look clickable (cursor:pointer, onclick, or focusable tabindex) 4. Text match priority inside each layer: exact → prefix → contains
Fill (input/textarea)
Locates the field by placeholder attribute, injects text using native value setter — compatible with React and Vue controlled inputs.
node ../scripts/browser-capture.mjs --tab-id "$TAB" --fill "Enter keyword::Module Federation"Select (dropdown)
Locates by placeholder attribute or default option text, then:
- Native `<select>` — sets value directly and dispatches
change - Custom dropdown — clicks the trigger to open, then clicks the matching option
node ../scripts/browser-capture.mjs --tab-id "$TAB" --select "Select environment::Production"When element is not found
Use --dump-dom to let Claude inspect the page and identify the correct selector:
node ../scripts/browser-capture.mjs --tab-id "$TAB" --dump-dom
# Claude analyzes the DOM, then:
node ../scripts/browser-capture.mjs --tab-id "$TAB" --click "#profile-nav-btn"Tab lifecycle
- New tab (no
--tab-id) → auto-closes unless--keep-tab - Existing tab (
--tab-id) → stays open unless--close
Browser Capture Setup
One-time setup: create a debug Chrome profile that shares your real cookies/auth.
---
Why a separate profile directory?
Chrome refuses to enable remote debugging on the default profile directory (~/Library/Application Support/Google/Chrome) as a security measure:
DevTools remote debugging requires a non-default data directory.The workaround: copy your real profile to a non-default path. Because macOS Chrome encrypts cookies using the system Keychain (Chrome Safe Storage key), a copied profile can still decrypt cookies — so you stay logged into all your sites.
---
One-time setup (macOS)
Step 1 — See which profiles you have:
node -e "
const fs = require('fs');
const base = process.env.HOME + '/Library/Application Support/Google/Chrome';
const state = JSON.parse(fs.readFileSync(base + '/Local State', 'utf8'));
const cache = state.profile.info_cache;
const last = state.profile.last_used;
console.log('Available Chrome profiles:\n');
Object.entries(cache).forEach(([dir, info]) => {
const tag = dir === last ? ' ← current' : '';
console.log(' ' + dir.padEnd(12) + info.name + tag);
});
console.log('\nDefault: ' + last);
"Step 2 — Sync the chosen profile to the debug location:
# Set PROFILE to the dir you want (default: current profile, usually "Default")
PROFILE="Default" # ← change to e.g. "Profile 1" if needed
REAL="$HOME/Library/Application Support/Google/Chrome/$PROFILE"
DEBUG_DIR="$HOME/Library/Application Support/Google/ChromeDebug"
mkdir -p "$DEBUG_DIR"
rsync -a --delete "$REAL/" "$DEBUG_DIR/Default/"
echo "Debug profile ready: $DEBUG_DIR (sourced from $PROFILE)"Note: rsync --delete is incremental — fast after the first sync. Re-run any timesessions have expired. The Keychain entry (Chrome Safe Storage) is shared, soencrypted cookies still decrypt correctly from the copied profile.
---
Launch with remote debugging
CHROME=$(find /Applications ~/Applications -name "Google Chrome" -path "*/MacOS/Google Chrome" 2>/dev/null | head -1)
DEBUG_DIR="$HOME/Library/Application Support/Google/ChromeDebug"
killall "Google Chrome" 2>/dev/null; sleep 1
"$CHROME" --remote-debugging-port=9222 --user-data-dir="$DEBUG_DIR" &One-time alias — add to ~/.zshrc:
# Usage: chrome-debug [ProfileDir] e.g. chrome-debug "Profile 1"
chrome-debug() {
local PROFILE="${1:-Default}"
local REAL="$HOME/Library/Application Support/Google/Chrome/$PROFILE"
local DEBUG_DIR="$HOME/Library/Application Support/Google/ChromeDebug"
local CHROME=$(find /Applications ~/Applications -name "Google Chrome" -path "*/MacOS/Google Chrome" 2>/dev/null | head -1)
echo "Syncing profile: $PROFILE → $DEBUG_DIR"
mkdir -p "$DEBUG_DIR"
rsync -a --delete "$REAL/" "$DEBUG_DIR/Default/"
killall "Google Chrome" 2>/dev/null; sleep 1
"$CHROME" --remote-debugging-port=9222 --user-data-dir="$DEBUG_DIR" &
echo "Chrome launching with debug profile (port 9222)..."
}Then run (using current/default profile):
chrome-debugOr pick a specific profile:
chrome-debug "Profile 1"---
Verify
curl -s http://localhost:9222/json/versionShould return something like:
{
"Browser": "Chrome/124.0.0.0",
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser/..."
}---
Prerequisites
- Node.js 21+ — required for built-in WebSocket support
Usage
node ../scripts/browser-capture.mjs "<url>" [timeout_ms] [--vars var1,var2,...]- Default timeout: 15 000ms
- Script opens a new tab in your Chrome debug profile (inherits its cookies/auth), navigates to the URL, waits for network to settle, then returns logs as JSON
- The tab is automatically closed after capture
---
Troubleshooting
`DevTools remote debugging requires a non-default data directory` → Chrome blocks debugging on the default profile. Follow the one-time setup above to create ~/Library/Application Support/Google/ChromeDebug.
Connection refused / port still closed → open -na silently reuses the existing Chrome process. Use the binary path approach above.
Cookies expired / not logged in after copying profile → Re-run the profile copy command to refresh it from your real profile.
`Node.js 21+ required` error → Upgrade Node.js: nvm install 21 && nvm use 21 (or install from nodejs.org).
`PUT /json/new` fails (older Chrome) → Try downgrading to Node's fetch with GET /json/new — edit the fetch(..., { method: 'PUT' }) line in browser-capture.mjs to remove the method option (defaults to GET).
Page loads but no logs captured → The page may have errored before CDP attached. Try increasing timeout, or check if the error only triggers on user interaction.
Single Capture
Navigate to a URL, collect logs + variables, tab auto-closes.
Usage
node ../scripts/browser-capture.mjs "<url>" [timeout_ms] [--vars var1,var2,...] [--wait-until auto|domcontentloaded|networkidle|timeout] [--action-wait auto|networkidle|domcontentloaded|timeout|none] [--no-entries]Default auto:
- Navigation phase prefers
domcontentloaded(interaction/variable capture scenarios); pure log collection falls back tonetworkidle - When not explicitly specified,
networkidleuses a shorter wait ceiling to avoid being slowed down by polling-heavy pages
Increase timeout for slow pages or heavy SPAs:
node ../scripts/browser-capture.mjs "https://example.com/dashboard" 30000Capture JavaScript variables:
node ../scripts/browser-capture.mjs "https://example.com" 20000 --vars __FEDERATION__,__NEXT_DATA__,featureFlagsCapture deep path variables:
node ../scripts/browser-capture.mjs "https://example.com" 20000 --vars __VMOK__.__INSTANCES__,window.__APP_STATE__.userPerformance-first capture (skip heavy entries and avoid long waits):
node ../scripts/browser-capture.mjs "https://example.com" 12000 --vars __VMOK__.__INSTANCES__ --action-wait none --no-entriesOutput format
{
"url": "https://example.com/dashboard",
"capturedAt": "2026-03-20T10:00:05.123Z",
"actionWait": "networkidle",
"timings": {
"navigateMs": 1832,
"clickMs": null,
"fillMs": null,
"selectMs": null,
"evalMs": null,
"varsMs": 42,
"totalMs": 2051
},
"total": 42,
"errors": 3,
"warns": 5,
"variables": {
"__FEDERATION__": {
"exists": true,
"value": { "runtime": "webpack" },
"skippedPaths": [
{ "path": "__FEDERATION__.snapshotHandler.HostInstance", "reason": "circular", "circularRef": "__FEDERATION__.snapshotHandler" },
{ "path": "__FEDERATION__.moduleCache.init", "reason": "function", "detail": "init" }
]
},
"__NEXT_DATA__": { "exists": false, "skippedPaths": [] }
},
"entries": [
{ "t": "2026-03-20T10:00:01.234Z", "level": "error", "msg": "Cannot read properties of undefined (reading 'user')", "stack": "https://example.com/assets/app.js:1:84231" },
{ "t": "2026-03-20T10:00:02.100Z", "level": "warn", "msg": "[HTTP] 404 Not Found — https://api.example.com/user/profile", "stack": null }
]
}Performance tips
- When only checking variables, add
--no-entriesto avoid excessive log volume. - When a stable selector is available, prefer an exact CSS selector to reduce click retries.
Log levels captured
error/warn/log/info/debug— fromconsole.*error— uncaught JS exceptions (includes stack trace when available)warn/error— HTTP 4xx / 5xx responseserror— network failures (CORS, DNS, connection refused)warn/error— browser-native entries (CSP violations, deprecations)
Variable serialization
Non-serializable values are handled gracefully:
- Circular references →
[Circular -> path] - Functions →
[Function: name] - Depth > 5 →
[max depth] skippedPathsrecords every property that could not be fully serialized
Sub-skill: config-check
Check Module Federation build configuration: verify correct MF plugin for the bundler, async entry configuration, exposes key format, and exposes path existence.
Step 1: Collect MFContext
Read and follow the instructions in ./context.md, passing ARGS as the project root.
Step 2: Run config check script
Serialize MFContext to JSON and pass it to the check script:
node scripts/config-exposes-check.js --context '<MFContext-JSON>'Process each item in the output results array:
CONFIG-PLUGIN · warning — incorrect or missing MF plugin
- Based on the detected bundler and installed packages, the recommended plugin is:
- Webpack only:
@module-federation/enhancedor@module-federation/enhanced/webpack - Vite only:
@module-federation/vite(MF options can live in rootmodule-federation.config.*viacreateModuleFederationConfig) - Rspack only:
@module-federation/enhanced/rspack(recommended) or@module-federation/rspack - Rsbuild:
@module-federation/rsbuild-plugin(recommended), or the Rspack plugin - Modern.js:
@module-federation/modern-js-v3for@modern-js/app-tools≥ 3.0.0, otherwise@module-federation/modern-js; falls back to Rspack/Webpack plugin based on the underlying bundler - Next.js:
@module-federation/nextjs-mf - Show the detected bundler, installed MF-related packages, and the recommended plugin
CONFIG-ASYNC-ENTRY · warning — async entry not configured (maps to RUNTIME-006)
experiments.asyncStartup = trueis not set in the bundler config- This setting is required by most bundler setups to avoid runtime initialization errors
- Exception: not required when using
@module-federation/modern-js-v3or@module-federation/modern-js - Note: Rspack requires version > 1.7.4 to support this option
- Reference: https://module-federation.io/blog/hoisted-runtime.md
- To check: read
bundler.configFilefrom MFContext and look forexperiments.asyncStartup
CONFIG-EXPOSES-KEY · warning — key does not start with `./`
- MF spec requires exposes keys to start with
./(e.g.,"./Button"not"Button") - Inform the user of the specific key name and guide them to correct the format
CONFIG-EXPOSES-PATH · warning — path does not exist
- The file referenced by the exposes value does not exist in the project. Show the specific key and incorrect path.
- The check must match the exact file extension (e.g.,
.tsx≠.ts) - Common causes:
1. Typo in file path 2. Wrong file extension 3. Incorrect relative path base (should be relative to project root)
When results is empty
- Inform the user that plugin selection, async entry config, and exposes passed all checks
Sub-skill: context
Collect the current project's Module Federation context (MFContext) from ARGS (defaults to the current working directory if empty), then output the aggregated summary.
1. Basic Info
Read {projectRoot}/package.json and extract:
name: project name- Merge
dependencies+devDependenciesinto a full dependency map
Detect the package manager (check files in order):
pnpm-lock.yaml→ pnpmyarn.lock→ yarnpackage-lock.json→ npm
2. Bundler & MF Config
Find config files in the following priority order (.ts / .mts take precedence over .js / .mjs / .cjs):
| Priority | Filename |
|---|---|
| 1 | module-federation.config.{ts,mts,js,mjs,cjs} |
| 2 | rsbuild.config.{ts,mts,js,mjs,cjs} |
| 3 | rspack.config.{ts,mts,js,mjs,cjs} |
| 4 | modern.config.{ts,mts,js,mjs,cjs} |
| 5 | next.config.{ts,mts,js,mjs,cjs} |
| 6 | webpack.config.{ts,js} |
| 7 | vite.config.{ts,mts,js,mjs,cjs} |
Read the first matched file and extract the remotes, exposes, and shared fields.
Determine the bundler name from the config filename (rspack / rsbuild / webpack / vite / next). When priority 1 (module-federation.config.*) is matched, scan the project root for bundler config files in priorities 2–7 to set bundler.name and bundler.configFile (the bundler config path, not the MF config path). If no bundler config is found, set bundler.name to unknown and bundler.configFile to the module-federation.config.* path.
3. Determine MF Role
| Condition | Role |
|---|---|
Has remotes and exposes | host+remote |
Only remotes | host |
Only exposes | remote |
| Neither | unknown |
4. Recent Error Event (optional)
Check if .mf/observability/latest.json exists; if so, read its contents.
5. Build Artifacts (optional)
Check if dist/mf-manifest.json and dist/mf-stats.json exist; if so, read them.
---
Aggregate the above information and output the MFContext summary in the following structure:
project:
name, packageManager, mfRole
bundler:
name, configFile
mfConfig:
remotes, exposes, shared
dependencies:
(list installed packages related to MF and their versions)
latestErrorEvent: (if present)
buildArtifacts: (if present)Sub-skill: docs
Answer Module Federation questions by fetching only the relevant documentation pages — not the entire docs.
Requires internet access to fetch documentation from module-federation.io.
Step 1: Fetch the documentation index
https://module-federation.io/llms.txtThe index is in this format:
## Section Name
- [Page Title](/path/to/page.md): brief description of the page contentStep 2: Identify the relevant page(s)
Read the page descriptions in the index and select the 1–3 pages most relevant to the user's question. Use the quick topic map below to narrow down candidates before reading descriptions.
Quick topic map:
| User asks about | Look in section |
|---|---|
| What is MF / concepts / glossary / getting started | Guide → start/ |
| CLI, CSS isolation, type hints, data fetching, prefetch | Guide → basic/ |
Runtime API, loadRemote, MF instance, runtime hooks | Guide → runtime/ |
| Build plugin setup for Webpack / Rspack / Rsbuild / Vite / Metro | Guide → build-plugins/ |
| Next.js / Modern.js / Angular / React integration | Guide → framework/ or Practice → frameworks/ |
| React Bridge / Vue Bridge / cross-framework rendering | Practice → bridge/ |
name, filename, exposes, remotes, shared, dts, manifest, shareStrategy | Configuration |
| Runtime plugins, retry plugin, custom plugin | Plugins |
| Performance, tree shaking, shared scopes | Guide → performance/ or Guide → advanced/ |
| Debug mode, Chrome DevTool, global variables | Guide → debug/ |
| Error messages, build errors, type errors | Guide → troubleshooting/ |
| Monorepo, Nx | Practice → monorepos/ |
| Deployment, Zephyr | Guide → deployment/ |
Step 3: Fetch the specific page(s)
Construct the URL by removing the .md extension from the path in the index, then prepend the base URL:
https://module-federation.io{path_without_md_extension}Examples:
/guide/start/index.md→https://module-federation.io/guide/start/index/configure/shared.md→https://module-federation.io/configure/shared/guide/runtime/runtime-api.md→https://module-federation.io/guide/runtime/runtime-api
Fetch the page(s) and read the content.
Step 4: Answer the question
Answer based on the fetched content. If the answer spans multiple pages (e.g., config + runtime), fetch both. Do not load more than 3 pages per question.
Important notes
- Always fetch the index first — never guess page paths from memory
- If the index descriptions are insufficient to identify the right page, fetch the most likely candidate and check its content
- The docs cover MF 2.0 (
@module-federation/enhanced) — this is different from the older Webpack 5 built-in Module Federation - Next.js support is deprecated; inform the user if they ask about it
Sub-skill: integrate
Integrate Module Federation into an existing project — add provider (exposes modules) or consumer (loads remote modules) configuration.
Step 1: Detect project
Collect MFContext by reading and following the instructions in ./context.md, passing ARGS as the project root.
If no bundler can be detected (no rsbuild.config, rspack.config, webpack.config, modern.config, next.config, vite.config found), this is likely a new project. Tell the user:
This looks like a new project. Run the following command to scaffold a full Module Federation project:
>
```bash
npm create module-federation@latest
```
Then stop.
If MF is already configured (MFContext shows existing remotes or exposes), inform the user what is already configured and ask if they want to add/modify the configuration or stop.
---
Step 2: Gather parameters
Ask the user the following questions (combine into one AskUserQuestion call):
1. Role — What role should this app play?
consumer— loads modules from remote apps (default)provider— exposes modules to other appsboth— exposes modules and loads remote modules
2. App name — What should the MF name be for this app?
- Suggest the
namefield frompackage.json(snake_case, no hyphens). Hyphens are not allowed in MF names.
3. Role-specific:
- If consumer or both: Do you want to connect to the public demo provider to see MF working immediately, or configure your own remotes?
demo— use the public demo provider (default for consumers)custom— I'll specify my own remote URLs- If provider or both: What module(s) do you want to expose? Provide
key: pathpairs, e.g../Button: ./src/components/Button.tsx. If unsure, use'.' : './src/index'as a default.
---
Step 3: Build the MF config object
Construct the MF config based on the gathered parameters:
Remote entries (for consumer / both)
Demo provider (use when user chose demo):
remotes: {
'provider': 'rslib_provider@https://unpkg.com/module-federation-rslib-provider@latest/dist/mf/mf-manifest.json',
},The demo provider exposes a React component at 'provider'. The user can import it in their app:
import ProviderApp from 'provider';Custom remotes (use when user chose custom): Ask the user to provide remote entries in the format name: url, then use them as-is.
Exposes (for provider / both)
Use the entries provided by the user. Example:
exposes: {
'./Button': './src/components/Button.tsx',
},Shared deps
Read package.json to check which frameworks are present. Set singletons accordingly:
- If
react+react-dompresent: add both as{ singleton: true } - If
vuepresent: add as{ singleton: true } - If both (rare): add all as singletons
---
Step 4: Generate files
Apply the correct pattern for the detected bundler:
---
Rsbuild
Detected by: rsbuild.config.ts / rsbuild.config.js in project root.
4a. Create module-federation.config.ts
import { createModuleFederationConfig } from '@module-federation/rsbuild-plugin';
export default createModuleFederationConfig({
name: '<app-name>',
// exposes: { ... }, // provider / both only
// remotes: { ... }, // consumer / both only
shareStrategy: 'loaded-first',
shared: {
// react + react-dom or vue — from Step 3
},
});4b. Modify rsbuild.config.ts
+import { pluginModuleFederation } from '@module-federation/rsbuild-plugin';
+import moduleFederationConfig from './module-federation.config';
export default defineConfig({
plugins: [
pluginReact(),
+ pluginModuleFederation(moduleFederationConfig),
],
});4c. Install
pnpm add @module-federation/rsbuild-plugin---
Modern.js
Detected by: modern.config.ts / modern.config.js in project root.
4a. Create module-federation.config.ts
import { createModuleFederationConfig } from '@module-federation/modern-js-v3';
export default createModuleFederationConfig({
name: '<app-name>',
// exposes: { ... }, // provider / both only
// remotes: { ... }, // consumer / both only
shared: {
// react + react-dom or vue — from Step 3
},
});4b. Modify modern.config.ts
+import { moduleFederationPlugin } from '@module-federation/modern-js-v3';
export default defineConfig({
plugins: [
appTools(),
+ moduleFederationPlugin(),
],
});4c. For consumer: add type paths
Modify tsconfig.json to resolve remote types:
{
"compilerOptions": {
+ "paths": {
+ "*": ["./@mf-types/*"]
+ }
}
}4d. Install
pnpm add @module-federation/modern-js-v3---
Rspack
Detected by: rspack.config.ts / rspack.config.js in project root.
4a. Modify rspack.config.ts / rspack.config.js
+const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack');
module.exports = {
+ experiments: {
+ asyncStartup: true,
+ },
plugins: [
+ new ModuleFederationPlugin({
+ name: '<app-name>',
+ // exposes: { ... }, // provider / both only
+ // remotes: { ... }, // consumer / both only
+ shared: {
+ // from Step 3
+ },
+ }),
],
};Note: experiments.asyncStartup requires Rspack > 1.7.4.4b. Install
pnpm add @module-federation/enhanced---
Webpack
Detected by: webpack.config.ts / webpack.config.js in project root.
4a. Modify webpack.config.js
+const { ModuleFederationPlugin } = require('@module-federation/enhanced/webpack');
module.exports = {
+ experiments: {
+ asyncStartup: true,
+ },
plugins: [
+ new ModuleFederationPlugin({
+ name: '<app-name>',
+ filename: 'remoteEntry.js',
+ // exposes: { ... }, // provider / both only
+ // remotes: { ... }, // consumer / both only
+ shared: {
+ // from Step 3
+ },
+ }),
],
};4b. Install
pnpm add @module-federation/enhanced---
Next.js
Detected by: next.config.ts / next.config.mjs / next.config.js in project root.
Deprecation warning: @module-federation/nextjs-mf only supports Pages Router (not App Router) and is no longer actively maintained. For new projects, consider using Rsbuild or Modern.js instead.4a. Modify next.config.mjs
+import { NextFederationPlugin } from '@module-federation/nextjs-mf';
const nextConfig = {
webpack(config, options) {
+ config.plugins.push(
+ new NextFederationPlugin({
+ name: '<app-name>',
+ filename: 'static/chunks/remoteEntry.js',
+ // exposes: { ... }, // provider / both only
+ // remotes: { // consumer / both only
+ // remote: `remote@http://localhost:3001/static/${options.isServer ? 'ssr' : 'chunks'}/remoteEntry.js`,
+ // },
+ shared: {},
+ extraOptions: {
+ exposePages: true,
+ enableImageLoaderFix: true,
+ enableUrlLoaderFix: true,
+ },
+ })
+ );
return config;
},
};4b. Enable local Webpack
Add to .env.local:
NEXT_PRIVATE_LOCAL_WEBPACK=true4c. Install
pnpm add @module-federation/nextjs-mf webpack -D---
Vite
Detected by: vite.config.ts / vite.config.js in project root.
4a. Create module-federation.config.ts
import { createModuleFederationConfig } from '@module-federation/vite';
export default createModuleFederationConfig({
name: '<app-name>',
// exposes: { ... }, // provider / both only
// remotes: { ... }, // consumer / both only
shared: {
// react + react-dom or vue — from Step 3
},
});4b. Modify vite.config.ts
+import { federation } from '@module-federation/vite';
+import moduleFederationConfig from './module-federation.config';
export default defineConfig({
plugins: [
+ federation(moduleFederationConfig),
],
});4c. Install
pnpm add @module-federation/vite---
Step 5: Auto-insert remote component (consumer / both only)
Skip this step entirely for provider-only role.
Ask the user:
Do you want me to automatically add the remote component to your app's entry so you can see it working right away?
If the user says no, just show the code snippet as a reference and move on to Step 6.
If the user says yes:
5a. Locate the entry file
Search for the entry component file in this priority order:
| Bundler | Candidates (in order) |
|---|---|
| Rsbuild | src/App.tsx, src/App.jsx, src/App.js |
| Modern.js | src/routes/page.tsx, src/routes/page.jsx |
| Webpack / Rspack | src/App.tsx, src/App.jsx, src/App.js, src/index.tsx, src/index.jsx |
| Next.js | pages/index.tsx, pages/index.jsx, pages/index.js |
| Vite | src/App.tsx, src/App.jsx, src/App.js |
Read the first file that exists. If none found, tell the user which file to modify manually and show the snippet — do not attempt blind writes.
5b. Determine remote name and import path
Use the remote name from the config generated in Step 4:
- If demo provider: remote name is
provider, import path is'provider' - If custom remotes: use the first remote name the user specified
5c. Edit the entry file
Add the import at the top of the file (after existing imports) and render the component inside the existing JSX return.
For React (Rsbuild / Rspack / Webpack / Vite)
Add import after the last existing import line:
import ProviderApp from 'provider';Insert <ProviderApp /> inside the existing JSX return. Find a natural place — inside a <div>, after existing content. Do not restructure the component; just append the element.
For Modern.js (src/routes/page.tsx)
Same pattern — add import and render <ProviderApp /> in the returned JSX.
For Next.js (pages/index.tsx)
Same pattern — add import and render <ProviderApp /> in the returned JSX.
5d. Add TypeScript declaration (if TypeScript project)
Check if tsconfig.json exists. If it does, create src/remote.d.ts (or add to an existing src/declarations.d.ts / src/env.d.ts if present):
declare module '<remote-name>' {
const Component: React.ComponentType;
export default Component;
}Replace <remote-name> with the actual remote name (e.g., provider).
Provider: how to verify the exposed module
Tell the user that after running the dev server, the manifest will be available at:
- Rsbuild / Rspack / Webpack / Modern.js / Vite:
http://localhost:<port>/mf-manifest.json(enablemanifest: truein MF config if not already set) - Next.js:
http://localhost:<port>/static/chunks/remoteEntry.js
Another app can reference this app as a remote using:
remotes: {
'<app-name>': '<app-name>@http://localhost:<port>/mf-manifest.json',
},---
Step 6: Summary
Output a concise summary:
- What files were created or modified
- What packages were installed
- How to start the dev server (use existing script from
package.json) - Next steps (e.g., add more remotes, configure shared deps, set up type generation)
Sub-skill: module-info
Fetch metadata and manifest info for a remote Module Federation module — publicPath, remoteEntry, type file URLs, and the module's remotes/exposes/shared from its mf-manifest.json.
Two modes: 1. Consumer mode — inside a consumer project; pass only the remote name; entry URL is resolved from mfConfig.remotes 2. Standalone mode — outside a consumer project; pass the remote name plus its remoteEntry URL directly
Step 1: Parse ARGS
- First token →
<module-name> - If a second token looks like a URL (starts with
http) →<remoteEntry-url>(standalone mode); remaining tokens →[project-root] - Otherwise →
[project-root](consumer mode)
Step 2a — Consumer mode (no URL provided)
Collect MFContext by reading and following the instructions in ./context.md, passing [project-root] as the project root.
Then run:
node scripts/module-info.js --context '<MFContext-JSON>' --module '<module-name>'Step 2b — Standalone mode (URL provided)
Run with an empty context and the explicit URL:
node scripts/module-info.js --context '{}' --module '<module-name>' --url '<remoteEntry-url>'Step 3: Present the result
| Field | Description |
|---|---|
publicPath | Base URL of the remote |
remoteEntry | Full URL to remoteEntry.js |
typesZip | URL to @mf-types.zip |
typesApi | URL to @mf-types.api (shown only if present) |
hasSsr | Whether SSR build artifacts were detected |
exposes | Modules this remote exposes |
remotes | Remotes this module depends on |
shared | Shared dependencies declared by this module |
If result.error is set, surface it directly and stop.
Step 4 (conditional)
If the user explicitly asks to see the type declarations (e.g. "show me the types", "what types does it export"), fetch result.typesZip or result.typesApi and display the relevant type definitions.
Observability: Analyze Reports
Use this reference after you have an observability report, Chrome DevTools export, browser reader output, Node report file, or build observability file.
Read Fields In Order
Do not start from events.
Reports omit undefined fields. Treat an absent optional field as "not observed or not relevant" unless a specific failure guide says that field must be present.
Read:
1. diagnosis.status 2. diagnosis.title 3. diagnosis.ownerHint 4. diagnosis.errorCode 5. diagnosis.facts 6. diagnosis.actions 7. summary.outcome 8. summary.error 9. summary.phases 10. summary.flags 11. summary.shared 12. build 13. moduleInfo 14. events
Decide Loading Success
Use summary.outcome before reading raw events:
runtime-loaded: the remote module was loaded by Module Federation runtime.component-loaded: a component-level success signal was observed. This may be
a business markComponentLoaded signal or the producer calling the injected onMFRemoteLoaded callback.
shared-resolved: a shared dependency was resolved successfully. Read
shared.name, shared.provider, shared.requiredVersion, shared.selectedVersion, and shared.availableVersions to explain which provider and version were selected.
failed: loading failed. Usesummary.error,diagnosis.actions, and
failedPhase.
recovered: loading failed first, then a runtime fallback or recovery path
returned a result.
For shared loading, summary.outcome: "recovered" can also mean the runtime handled a custom shared-info miss. If summary.phases.shared.status is "complete" and shared.reason is "custom-share-info-unmatched", do not say Module Federation loading failed. Say the build plugin supplied customShareInfo, but no registered shared provider matched it, so the runtime continued through its handled path. Ask the user to inspect shared config only if they expected a specific provider/version to be selected.
The observability plugin cannot determine whether @module-federation/retry-plugin itself succeeded. It records the Module Federation loading chain and final resource result, not retry-plugin internal state. Do not infer retry success only from URL changes, retry-plugin console lines, or the page eventually rendering. If the user needs a retry success/failure judgment, ask them to add their own evidence in retry-plugin hooks such as onRetry, onSuccess, and onError, then read that output. Without those hook records or user-provided retry events, say only that the remote eventually loaded.
Use summary.loadCompleted only as "the loadRemote flow ended"; it does not by itself prove success. Use summary.runtimeLoaded for remote module success and summary.componentLoaded for component-level success.
summary.componentLoaded: false is not by itself proof that the React component failed to render. It only means no component-level ready signal was observed. If react.injectLoadedCallback: true is enabled but componentLoaded is false:
1. first inspect the producer source, if available, and check whether the component receives and calls props.onMFRemoteLoaded?.(...) 2. if the producer source is available and the callback is not called, explain that the remote resource loaded but component readiness is unknown until the producer adds the callback 3. if the producer source is not available, ask the user whether the producer calls onMFRemoteLoaded; do not claim the component failed only because the flag is false 4. only conclude that the component failed when there is additional evidence, such as a React error, an error boundary state, a missing expected UI, or a producer/runtime error event
If events are needed, remote module success is usually:
phase: "loadRemote"status: "success"lifecycle: "onLoad"message: "remote:loaded"
React callback injection is opt-in. It can inject an onMFRemoteLoaded prop into the remote React component by returning a wrapper component. If the producer calls that prop, treat the resulting component:business-loaded event as the producer's own ready signal. Do not infer React mount success from this plugin; it no longer observes React render lifecycle events. Because callback injection changes the component reference, treat it as a temporary production debugging switch and ask the user to remove it after the issue is fixed.
Shared Evidence Limits
Do not make shared dependency analysis the default path. If the user did not ask about shared dependencies and the report does not point to shared loading, do not rerun the reader just because shared fields are absent.
Shared dependency evidence is only expected when the page uses Module Federation >= 2.5.0 and the active runtime path emits shared observability events. If the version is unknown or below 2.5.0, say shared dependency details were not available from this report; do not treat that as a failed read and do not claim that shared dependencies are definitely fine.
Decide The Likely Owner
Use diagnosis.ownerHint first:
host: inspect host remotes, request id, manifest URL, and runtime call.remote: inspect producer exposes, remoteEntry type/global, and exposed module
execution.
shared: inspect host and remote shared config, selected provider, versions,
shareScope, singleton, strictVersion, and eager.
network: inspect URL reachability, CORS, status code, response body, timeout,
CDN, gateway, or proxy.
build: inspect build-report, bundler output, and observability output path.unknown: usesummary.phasesto find the first missing/error phase, then
inspect events.
Follow Diagnosis Actions
Treat diagnosis.actions as the prioritized checklist. For each action:
1. inspect the referenced config or runtime fact 2. compare it with diagnosis.facts 3. compare it with .mf/observability/build-info.json when build evidence is needed 4. make the smallest code/config fix 5. verify with the failing app or targeted test
Use Build Evidence Correctly
Runtime reports do not have summary.build.
Runtime reports also do not embed build facts. If build evidence is needed, read .mf/observability/build-info.json or .mf/observability/build-report.json as a separate file and compare it with the runtime report.
moduleInfo appears only for snapshot/moduleInfo dependent failures. It is a clipped view of __FEDERATION__.moduleInfo, not a full dump. Use only:
entries[].nameentries[].publicPathentries[].getPublicPathentries[].remoteEntryentries[].globalNametotalCountmatchedCountavailableNames
Large fields such as modules, shared, and assets are intentionally removed. The publicPath, getPublicPath, and remoteEntry locator fields preserve query/hash data and are only length-limited because deployment platforms often use those values to route a module.
Do not describe moduleInfo.availableNames as a component list, expose list, or prefetch list. It is the clipped list of deployment-provided moduleInfo remote candidate keys that were available when no entry matched the failed remote.
Use Runtime Error Codes As References
Runtime error codes are stable entry points, but this observability skill should not act as a full RUNTIME-xxx troubleshooting manual.
When a report contains diagnosis.errorCode, summary.error.errorCode, or a console RUNTIME-xxx code:
1. use the code to choose the relevant runtime troubleshooting document 2. use this report to confirm the actual phase, owner, request, remote, shared, moduleInfo, and timing evidence 3. avoid giving a fix based only on the code if report fields contradict it 4. if there is no observability report, ask for the report, trace id, browser reader output, Node report file, or enough console/network evidence
Keep the explanation centered on the report evidence. For code-specific definitions and fixes, refer to the runtime troubleshooting docs instead of duplicating that content here.
Final Response Shape
When reporting back, keep it concrete:
1. what report source was used 2. likely owner 3. the key evidence 4. what was changed or what should be changed 5. how it was verified
If the report is missing, route back to reference/observability-page.md or reference/observability-read.md instead of guessing from incomplete console text.
Observability: Page Observation
Use this reference for one-time browser page checks, such as "open/visit this URL", "看下 MF 加载情况", or live diagnosis with no report yet.
Route
- If the user already provided a report file, trace id,
read:command, or
browser reader expression, stop here and route to observability-read.md or observability-analyze.md.
- If a project path or local dev context is available, run Fast Integration
Check, then follow its result:
- Existing integration found: use the installed plugin report path in
observability-read.md. Run the Chrome Debug Preflight below before starting a collector, opening, or reloading the page. If collector config is found, start skills/mf/scripts/observability-collector.js only after the preflight succeeds, then open or reload the page and read the collected report files. Otherwise open the page and read the existing browser report with skills/mf/scripts/read-observability-report.mjs --scope auto. Do not inject another observability plugin, do not use skills/mf/scripts/open-observability-page.mjs, and do not try Playwright or project tests.
- Integration absent or unconfirmed: use Live Browser Injection.
- If no project context is available, use Live Browser Injection.
Do not read observability-use.md for one-time page checks unless the user asks to install or keep the plugin in the project.
Browser Control Rule
For one-time page checks, use a fixed Chrome debug profile by default. Do not try the Codex Chrome plugin, do not operate the user's current tab directly, and do not close or restart the user's daily Chrome. The fixed profile keeps its own cookies and local storage across runs, but it is separate from the user's daily Chrome profile. If the page needs login and this profile is not logged in yet, open the page first, then ask the user to log in inside that debug window.
Run only the minimal browser observation path:
1. Optionally run one cheap availability check such as curl -I <url> for a local URL. 2. Run Fast Integration Check when project context is available. 3. Run Chrome Debug Preflight with the fixed debug profile before starting a collector, opening, reloading, or injecting into the target page. 4. If the installed plugin uses collector output, start skills/mf/scripts/observability-collector.js, then open or reload the target page. If it uses browser-reader output, open the page with skills/mf/scripts/open-chrome-debug.mjs. 5. Read the installed plugin report through the collector files or skills/mf/scripts/read-observability-report.mjs. 6. Analyze the report. If the report is unavailable, follow the permission or restart branch below.
Do not consult memory, run project tests, create Cypress/Playwright files, use Electron/test browsers, use the Codex in-app Browser skill, or inspect broad source history as a substitute for the browser report. A local demo URL still uses this installed-plugin report path. Existing project tests can be mentioned only when the user explicitly asks to run tests.
Chrome Debug Preflight
Before any live page observation that needs to open, reload, inject into, or read a browser page, verify the fixed Chrome debug profile first:
node skills/mf/scripts/open-chrome-debug.mjs --url about:blank --timeout-ms 3000 --jsonThis preflight must happen before starting skills/mf/scripts/observability-collector.js. The helper creates and reuses ~/.chrome-debug-profiles/mf-obs by default, so Chrome can expose a debug port without closing the user's daily Chrome.
If running the preflight is blocked by local permissions, rerun the same helper with tool permission/escalation before starting a collector or page read. If it returns debug-port-unavailable, do not restart the user's daily Chrome. Ask the user to close the mf-obs debug Chrome window if it is already open, or choose another debug port.
Only after the preflight succeeds should you start a collector, open the target URL, reload the page, or run live injection.
For collector output, run the preflight first, start the collector, then run this helper to open or reload the page. For browser-reader output, run the preflight first, then open the page with this helper before reading the page:
node skills/mf/scripts/open-chrome-debug.mjs --url "<target-url>" --jsonUse the returned port, userDataDir, and, when present, pageId. If pageId is missing, use --url-contains "<target-url>" with the reader. The helper first reuses an existing Chrome debug port when available and opens the target URL there; otherwise it launches Chrome with the fixed debug profile and remote debugging enabled. Do not use the user's daily Chrome profile and do not create a fresh one-off /tmp profile unless the user explicitly asks for a stateless run.
Permission and restart handling are part of this flow:
- If running the helper or connecting to
127.0.0.1:<port>is blocked by the
Codex sandbox or local process permissions, immediately rerun the same built-in helper or reader with tool permission/escalation. Do not stop to ask the user for a business decision, and do not create ad hoc WebSocket or CDP scripts in /tmp.
- If the helper returns
debug-port-unavailable, do not close, restart, or kill
the user's daily Chrome. Ask the user to close the fixed debug-profile Chrome window if it is stuck, or choose another debug port.
- Do not answer "cannot determine" just because no debug port was available.
Either open the debug session through the helper, request tool permission for the same helper, or ask for debug-profile cleanup/another port.
After opening the target URL, classify page-open failures before asking the user to act:
- For localhost or local-network URLs, use a cheap request such as
curl -Ito
distinguish service-down, wrong port, DNS, TLS, or HTML-fallback problems.
- If the page opens but redirects to a login page, shows an auth wall, or the
app report says authentication is missing, ask the user to log in inside the fixed debug Chrome window, then reload and continue.
- If the page opens normally, continue directly to collector/read/analyze. Do
not ask the user to log in just because the fixed profile is separate.
Do not use Playwright, Cypress, Electron, project e2e tests, the Codex in-app Browser skill, or a generic browser automation runtime for the report read. The skill already provides CDP helpers and collector helpers for this flow.
Do not use a quick-completion heuristic for normal page observation. A single remote or shared event does not prove the page is finished, because Module Federation may be only one part of the page. Also do not take screenshots unless the user asks to inspect visual page state.
Fast Integration Check
Run this only when a project path or local dev context is available.
1. Read the nearest package.json. 2. If @module-federation/observability-plugin is absent, use Live Browser Injection. 3. If the dependency exists, inspect only existing common entry/config files for observability-plugin, createObservability, ObservabilityPlugin, runtimePlugins, plugins: [observability.plugin], collector, and browser: rsbuild.config.*, rspack.config.*, webpack.config.*, vite.config.*, module-federation.config.*, modern.config.*, edenx.config.*, src/observability*, src/runtime*, src/bootstrap*, src/main*, and local page files that import an observability helper.
ObservabilityBuildPluginis the build-side companion and does not replace
the runtime observability plugin. Do not conclude from ObservabilityBuildPlugin alone that the runtime report is unavailable. Continue the fast check in runtimePlugins, src/observability*, bootstrap, runtime, main, and page files before deciding whether runtime integration exists.
createObservability(...),ObservabilityPlugin(...), or
plugins: [observability.plugin] means runtime integration exists when connected to the runtime/init/createInstance path. 4. If both dependency and registration are found, treat the installed plugin as authoritative for this task:
- If
collector: trueorcollector: { enabled: true }is found, complete
Chrome Debug Preflight first, then route to observability-read.md, start skills/mf/scripts/observability-collector.js, and open or reload the target page.
- Otherwise complete Chrome Debug Preflight first, then open the page
through skills/mf/scripts/open-chrome-debug.mjs --url "<target-url>" --json and read the existing browser report with the built-in reader using --scope auto.
- Do not use Live Browser Injection, do not run
skills/mf/scripts/open-observability-page.mjs, and do not add a second observability plugin to a page that already registered one. 5. If only the dependency is found but no registration is found quickly, treat integration as unconfirmed. Do not block one-time diagnosis; use Live Browser Injection unless the user asked for project setup.
Do not run installs, builds, or broad repository searches for this decision. Full project auditing belongs to observability-use.md, not one-time browser diagnosis.
Live Browser Injection
Use this when the project is not already integrated, integration cannot be confirmed quickly, or no project path is available.
Use scripts/open-observability-page.mjs from the mf skill directory. The script reads a ready-to-run chrome-devtool IIFE, wraps it with the preset options below, registers the init script through Chrome CDP before navigation, opens the target URL, and prints the reader expression for the fixed chrome_extension scope. Run it after the fixed Chrome debug profile is ready.
Example:
node skills/mf/scripts/open-chrome-debug.mjs --url about:blank --json
node skills/mf/scripts/open-observability-page.mjs \
--url "https://example.com/" \
--port "<returned-port>" \
--output "/tmp/mf-observability-open.json" \
--jsonThe default IIFE path is assets/observability-chrome-devtool.iife.js inside this skill. It is copied into the skill so live diagnosis does not need a runtime build step. If a newer plugin build is needed, pass --iife <file> or set MF_OBSERVABILITY_IIFE. Do not install the plugin into the user's project unless the user asked for project setup.
The IIFE must expose ChromeObservabilityPlugin from the plugin's chrome-devtool entry. The script does not run esbuild, does not import the package at runtime, and does not generate a bundle during diagnosis. It presets these options:
ChromeObservabilityPlugin({
level: 'verbose',
console: true,
browser: {
enabled: true,
mode: 'development',
},
trace: {
printStart: true,
},
devtools: {
enabled: true,
source: 'module-federation/observability',
},
});Do not pass browser.scope. The browser reader scope is fixed to chrome_extension by the chrome-devtool export. The script reads the latest report once before it exits and stores that in initialRead. Use initialRead first. After additional user interaction or a later reload, route to observability-read.md and reread window.__FEDERATION__.__OBSERVABILITY__.chrome_extension.
After running skills/mf/scripts/open-observability-page.mjs, immediately open the JSON written by --output and inspect these fields before doing anything else:
reportSource
initialReadAvailable
initialReadReportCount
initialReadLatestTraceId
initialRead
initialReadExceptionDetails
nextReadCommandIf initialReadAvailable is true, analyze initialRead directly and save that output as the raw evidence for the current answer. Do not run another reader just to confirm the same page load.
If initialReadAvailable is false, explain the initialRead.readError, initialReadExceptionDetails, available scopes, and injection status from the same output JSON. Then use nextReadCommand only if the page later receives user interaction, reloads, or the user asks for a specific follow-up target.
Use the nextReadCommand / readCommand printed by this script only when a later read is needed, or run the built-in reader directly:
node skills/mf/scripts/read-observability-report.mjs \
--port "<returned-port>" \
--page-id "<opened-page-id>" \
--scope chrome_extension \
--output "/tmp/mf-observability-report.json" \
--jsonNever create an ad hoc reader such as /tmp/read-mf-report.mjs or /private/tmp/read-*-cdp.*. The live-injection path already has two supported report sources: initialRead from skills/mf/scripts/open-observability-page.mjs, then skills/mf/scripts/read-observability-report.mjs for later reads.
Observability: Read Reports
Use this reference to get Module Federation observability reports from the most direct available source.
If the user asks to open or visit a real page and observe Module Federation loading with no existing report, use reference/observability-page.md first so it can choose existing project integration or temporary browser injection.
If the report source is an already installed browser reader or a temporary browser injection from reference/observability-page.md, use skills/mf/scripts/read-observability-report.mjs first. Do not create a one-off CDP or WebSocket reader script. Do not try Playwright, Cypress, Electron, project e2e tests, the Codex in-app Browser skill, or any generic browser runtime before this built-in reader.
When reference/observability-page.md found an installed observability plugin, that installed plugin is the evidence source for the current task. Do not switch to temporary browser injection if the installed report is missing or hard to read. Use the collector path when the project is configured for collector output, use the browser-reader path when it is configured for browser output, or stop and ask for an accessible plugin report/export when neither channel is available.
If skills/mf/scripts/open-observability-page.mjs already returned initialRead, analyze that result before running another read. Treat initialRead as the first report source for temporary injection. Rerun the reader only after user interaction, reload, or a specific follow-up target.
For temporary injection, the only allowed read paths are:
1. the initialRead object already saved by skills/mf/scripts/open-observability-page.mjs 2. the built-in skills/mf/scripts/read-observability-report.mjs command printed as nextReadCommand / readCommand
Do not write or run one-off scripts such as /tmp/read-mf-report.mjs, /private/tmp/read-*-cdp.*, custom WebSocket clients, or custom CDP readers. If a local-port permission error occurs, rerun the same built-in helper or reader with permission/escalation.
Installed Plugin Fast Path
When reference/observability-page.md found that the project already registered the observability plugin, first decide which installed-plugin output channel is configured. Do not use skills/mf/scripts/open-observability-page.mjs in this path.
Installed Plugin Collector Path
Use this when the project config contains collector: true or collector: { enabled: true }. Before starting the collector, make sure reference/observability-page.md already completed Chrome Debug Preflight. If you entered this file directly, run the preflight first:
node skills/mf/scripts/open-chrome-debug.mjs --url about:blank --timeout-ms 3000 --jsonIf running the helper is blocked by local permissions, rerun the same helper with tool permission/escalation. If it returns debug-port-unavailable, do not restart the user's daily Chrome. Ask the user to close the fixed debug-profile Chrome window if it is stuck, or choose another debug port. Do not start the collector until Chrome debug access is ready.
After the preflight succeeds, start the local node collector before opening or reloading the page:
node skills/mf/scripts/observability-collector.js --port 17891If the project config uses a custom collector port, use that port. If the port is occupied, pick the next free local port and tell the user the project config must match it before the collector can receive reports.
Then open or reload the target page through skills/mf/scripts/open-chrome-debug.mjs. After the page reproduces the loading path, read these files first:
.mf/observability/collector/latest-session.json
.mf/observability/collector/<sessionId>/latest-report.jsonIf latest-report.json is not present yet, read .mf/observability/collector/<sessionId>/latest.json and .mf/observability/collector/<sessionId>/events.jsonl to explain whether the plugin has not posted yet, the trace is still pending, or the collector received only raw updates. Stop the collector after analysis unless the user asks to keep it running.
If the collector receives nothing, do not inject a temporary plugin. Report that the installed plugin did not post to the local collector and ask for the project's collector config, onReport output, or browser-reader access.
Installed Plugin Browser Reader Path
Use this when the project config exposes the browser reader, or when collector is not configured:
node skills/mf/scripts/read-observability-report.mjs \
--port "<returned-port>" \
--page-id "<opened-page-id>" \
--scope auto \
--limit 10 \
--output "/tmp/mf-observability-report.json" \
--jsonIf pageId is missing from the open step, use --url-contains "<target-url>" instead of --page-id.
--scope auto reads the page's existing window.__FEDERATION__.__OBSERVABILITY__ scopes and selects chrome_extension when present, otherwise the first available scope. This avoids an extra "inspect scopes, then rerun" step for locally installed plugins that use project-defined scopes such as runtime_host.
If this browser read is blocked, rerun the same built-in reader with permission. If the read still cannot access a report, do not inject a temporary plugin into an already integrated page. Use the collector only when the installed plugin is configured for collector output, otherwise ask for exportReport(traceId), the app's onReport upload payload, or a pasted report JSON.
Browser Capability Check
For a browser page, first use the built-in CDP reader or another backend that can evaluate JavaScript in the page context:
1. open or inspect the target page 2. evaluate JavaScript in the page context through the built-in reader when a Chrome debug port is available 3. if evaluation works, read window.__FEDERATION__.__OBSERVABILITY__ directly and do not start the local collector
Use the local collector when the installed plugin is configured for collector output, or when the user explicitly wants a repeatable local collector loop. Do not start it for a page that has no installed plugin collector config and then use the lack of collector output as evidence.
Browser Console Or Page Global
If the console hint includes a read: line, execute that command exactly in the browser console context:
window.__FEDERATION__.__OBSERVABILITY__['runtime_host'].getReport('mf-...');For temporary browser injection through reference/observability-page.md, always try chrome_extension first:
window.__FEDERATION__.__OBSERVABILITY__['chrome_extension'].getLatestReport();
window.__FEDERATION__.__OBSERVABILITY__['chrome_extension'].getReports({
limit: 10,
});Do not pass or invent a custom scope for this injected path. If the app itself configured a different browser scope, inspect:
Object.keys(window.__FEDERATION__.__OBSERVABILITY__);For temporary browser injection, read and save reports with the built-in script from the mf skill directory. Use the port and pageId printed by scripts/open-observability-page.mjs:
node skills/mf/scripts/read-observability-report.mjs \
--port "<returned-port>" \
--page-id "<opened-page-id>" \
--scope chrome_extension \
--limit 10 \
--output "/tmp/mf-observability-report.json" \
--jsonDo not run this command immediately after skills/mf/scripts/open-observability-page.mjs when its output has initialReadAvailable: true. In that case, analyze initialRead first and only rerun the reader after a click, route change, reload, wait-for-later-state, or user-requested filter.
Use --trace-id, --remote, --expose, or --shared when the user names a specific trace or target. If the local Chrome debug connection is blocked by sandbox or local process permissions, rerun this same built-in reader with permission. If no debug port exists, return to reference/observability-page.md and use skills/mf/scripts/open-chrome-debug.mjs; if that helper reports debug-port-unavailable, ask the user to close the fixed debug-profile Chrome window if it is stuck, or choose another debug port. Do not create a temporary WebSocket/CDP reader script.
If the user only has the latest browser report, use:
window.__FEDERATION__.__OBSERVABILITY__['runtime_host'].getLatestReport();If the user wants to inspect loading chains, observe MF loading, or debug a page that stays in loading state without an error or traceId, do not wait for a console error. Read recent reports directly:
window.__FEDERATION__.__OBSERVABILITY__['runtime_host'].getReports({ limit: 10 });Then look for reports with status: "pending" or summary.outcome: "pending". Use startedAt, updatedAt, duration, summary.phases, and diagnosis.pendingPhases to explain which phase has started, which phase has not completed, and how long the trace has been idle. When the browser reader is enabled in development mode, the console prints Observability trace started lines for loadRemote and loadShare by default; use the printed traceId to read that exact report. In production browser mode, these start logs are disabled unless the app explicitly sets trace.printStart: true.
For recent or filtered browser reports, use:
window.__FEDERATION__.__OBSERVABILITY__['runtime_host'].getReports({ limit: 5 });
window.__FEDERATION__.__OBSERVABILITY__['runtime_host'].findReports({
remote: 'remote1',
});
window.__FEDERATION__.__OBSERVABILITY__['runtime_host'].findReports({
expose: './Button',
});
window.__FEDERATION__.__OBSERVABILITY__['runtime_host'].findReports({
shared: 'react',
});
window.__FEDERATION__.__OBSERVABILITY__['runtime_host'].exportReport('mf-...');If the browser global is disabled, ask for the app's onReport output, uploaded observability record, or the full report output pasted by the user.
If the browser console only contains traceId and errorCode, treat it as production-safe output. Do not assume the full report is globally readable. Ask for an explicit exportReport(traceId) output, the app's onReport output, or the user's uploaded observability record.
Shared Dependency Read Boundary
Do not run another browser read only because the report has no shared dependency records. Only use --shared or rerun for shared data when the user explicitly asks about shared dependencies, names a shared package, or the report/error points to shared loading.
Shared dependency evidence is only expected when the page uses Module Federation >= 2.5.0 and that runtime path emits shared observability events. If the MF version is missing, unknown, or lower than 2.5.0, absence of summary.shared, shared-resolved, or shared events is not a reason to reread the page and is not proof that shared dependencies are healthy.
Chrome DevTools Export
If the user has the Module Federation Chrome extension, use the Loading Trace tab to inspect or export the same reports. If the page already registered its own observability plugin, the tab reads those reports. If not, the tab can start temporary collection for the current tab.
When the user provides an exported JSON file from Chrome DevTools, read that file as the report source and then use reference/observability-analyze.md.
Local Collector
If evaluation is unavailable and the project enables ObservabilityPlugin({ collector: true }) or collector: { enabled: true, port }, complete Chrome Debug Preflight from reference/observability-page.md first, then start the lightweight collector before opening the page.
Before starting the collector, check whether the configured port is available. Use 17891 by default. If it is occupied, pick the next free local port (17892, 17893, ...), then tell the user to make the runtime plugin config match that port:
ObservabilityPlugin({
collector: {
enabled: true,
port: 17892,
},
});Start the collector with the selected port:
node skills/mf/scripts/observability-collector.js --port 17891The collector listens only on 127.0.0.1, receives browser reports from the runtime plugin, and writes:
.mf/observability/collector/latest-session.json
.mf/observability/collector/<sessionId>/events.jsonl
.mf/observability/collector/<sessionId>/latest.json
.mf/observability/collector/<sessionId>/latest-report.jsonAfter opening the target page and reproducing the issue, read latest-report.json first. If the page is still loading, read latest.json or events.jsonl to inspect pending traces. Stop the collector process after the analysis unless the user asks to keep collecting.
Node Or SSR
Read .mf/observability/latest.json first. Use .mf/observability/events.jsonl only if multiple traces or event ordering are needed.
latest.json is the formatted latest complete report. events.jsonl is an append-only stream where each line is one runtime event with its own traceId, timestamp, phase, status, context, and error fields when present.
Build
Read .mf/observability/build-report.json for build failures and .mf/observability/build-info.json for successful build facts.
Observability: Use And Enable
Use this reference when the user asks how to enable Module Federation loading observability, how to upload reports, or when to recommend the observability plugin.
For a one-time request to open a page and inspect current loading, use reference/observability-page.md instead. Do not ask the user to install the plugin just to inspect one live page.
Version And Scope
@module-federation/observability-plugin is designed for Module Federation 2.5.0 and later. When recommending this plugin, recommend upgrading MF to 2.5.0+ at the same time.
If the app is not upgraded or does not use @module-federation/observability-plugin, this skill still works in error-code mode: diagnose from the error code, console text, URL, network evidence, and runtime/build config. Only ask for an observability report when the plugin is actually enabled or the user can enable it.
If the user only knows that MF failed, but has no observability report and no accurate RUNTIME-xxx code or actionable console/network evidence, ask whether they can follow the recommended path: upgrade Module Federation to 2.5.0+ and enable @module-federation/observability-plugin to collect a report with loading phase, owner, shared, and moduleInfo evidence. If build evidence is needed, ask for the separate .mf/observability/build-info.json or .mf/observability/build-report.json file.
Development Usage
Use development observability when the goal is to understand a loading chain:
- which MF instance loaded a remote or shared dependency
- which remote/expose was requested
- which shared provider/version was selected
- whether a trace is pending, successful, failed, or recovered
- which
traceIdshould be used for deeper analysis
In development browser mode, start logs for loadRemote and loadShare are enabled by default. They print enough information for an agent to read the current report even when the page stays in loading state.
Production Usage
Production usage should usually keep console output small and send reports to the application's own telemetry system.
Use plugin callbacks for upload or custom logging:
onReport: called when a report is generated or updated; use this for report
upload and long-term storage.
onEvent: called for raw timeline events; use this only when the application
needs event-level telemetry.
In production browser mode, start logs are disabled by default. Enable trace.printStart: true only when the team intentionally wants trace ids in the console for live debugging.
Do not assume the full browser report is globally readable in production. If the browser console only contains a traceId and errorCode, ask for the uploaded record, onReport payload, or explicit export output.
Chrome Extension
The Module Federation Chrome extension provides a Loading Trace tab for the same report workflow.
Use it when the user wants to try the feature quickly, inspect reports visually, or export a report for an AI coding agent:
- If the page already registered its own observability plugin, the tab reads the
page reports and shows them as custom reports.
- If the page has not installed the plugin, the tab can start temporary
collection for the current tab.
- Exported Chrome DevTools reports should be analyzed with
reference/observability-analyze.md.
When documenting or recommending the extension, link to the latest Chrome extension page and the Chrome Devtool Loading Trace docs.
For agent-driven live page checks, reference/observability-page.md can inject the same chrome-devtool observability entry before navigation and then route to reference/observability-read.md.
After the user reproduces the issue with an installed browser reader, route to reference/observability-read.md and read the page with skills/mf/scripts/read-observability-report.mjs --scope auto. Do not start the collector for this browser-reader setup unless the user asks for a collector loop or page reading is blocked.
obs (Observability)
Use this sub-skill as the single entry for Module Federation observability plugin work.
Do not put the full workflow here. Decide the user's current phase, then load the smallest needed reference below.
Treat obs as shorthand for observability in user prompts, reports, file names, and follow-up requests.
Route
Project Setup
Use observability-use.md when the user asks how to install, enable, configure, upload, keep a long-term dev loop, or recommend the observability plugin for a project.
Typical triggers:
- enable observability
- install observability
- observability setup
- obs setup
- onReport
- onEvent
- production telemetry
- Chrome extension
- 怎么接入观测插件
- 启用观测
- 使用观测插件
- 生产环境上报
Page Observation
Use observability-page.md when the user asks to open/visit a live page, gives a URL, asks to inspect current Module Federation loading, or has a loading problem but no report yet.
Typical triggers:
- open page
- visit URL
- observe page
- browser observability
- obs
- mf obs
- debug current page
- no report
- 看下 MF 加载情况
- 看下 Module Federation 加载情况
- 观测页面
- 没有报告
Read
Use observability-read.md when the user provides a trace id, a console read: command, a browser reader expression, or asks the agent to read reports from a live page, Chrome DevTools export, local collector, Node/SSR output, or build output. For a live browser page with an installed observability plugin, use the installed plugin report: start the local collector when the project config enables collector output, otherwise read the existing browser report. Do not switch to temporary browser injection after detecting an installed plugin.
Typical triggers:
traceIdread:getReportgetLatestReportgetReportsfindReportswindow.__FEDERATION__.__OBSERVABILITY__collector.mf/observability/latest.json.mf/observability/events.jsonl.mf/observability/build-info.json.mf/observability/build-report.json- Chrome DevTools export
- 本地收集
- 读取报告
- 加载链路
- 导出报告
After reading the report, continue with observability-analyze.md.
Analyze
Use observability-analyze.md when the user provides a report JSON/file or asks what an observability report means.
Typical triggers:
- observability report
- obs report
Observability report generateddiagnosissummary.phasessummary.outcomeownerHintmoduleInfosharedshared-resolvedevents- pending loading
- recovered loading
- 观测报告
- 分析报告
- 判断是谁的问题
If the report is missing or incomplete, route back to observability-read.md or observability-page.md.
Order With Related MF Tools
Runtime error codes are still the stable first signal. If a report contains RUNTIME-xxx, analyze the report first, then reference the matching runtime diagnostic sub-skill for the code definition.
If there is only a RUNTIME-xxx code and no observability report, do not claim that a report was read. Use the runtime error-code path first, then recommend enabling ObservabilityPlugin when the evidence is too thin to identify the owner or exact phase.
If the report comes from the Chrome extension entry and the runtime version is older, missing, or a preview build, do not assume shared events should be present. Diagnose shared issues from error codes, console errors, and configuration evidence instead.
Sub-skill: perf
Check Module Federation local development performance configuration: detect whether recommended performance optimization options are enabled to alleviate slow HMR and slow build speed.
Step 1: Collect MFContext
Read and follow the instructions in ./context.md, passing ARGS as the project root.
Step 2: Run performance check script
Serialize MFContext to JSON and pass it to the check script:
node scripts/performance-check.js --context '<MFContext-JSON>'Provide recommendations for each item in the output results and context.bundler.name:
PERF · info — `dev.disableAssetsAnalyze` (applies to all projects)
- Disabling bundle size analysis during local development significantly improves HMR speed
- Add to the Rsbuild config:
dev: { disableAssetsAnalyze: true }PERF · info — Rspack `splitChunks` optimization (shown only when bundler.name is rspack or rsbuild)
- Setting
splitChunks.chunksto"async"reduces initial bundle size and speeds up first-screen loading - Add to the build config:
output: { splitChunks: { chunks: 'async' } }PERF · info — TypeScript DTS optimization (shown only when typescript dependency is detected)
- If type generation (DTS) is the main bottleneck, options include:
1. Temporarily disable DTS: set dts: false in the @module-federation/enhanced config 2. Switch to ts-go for significantly faster type generation
Step 3: ts-go migration (interactive)
After presenting the DTS recommendation, ask the user:
"Would you like me to automatically try switching to ts-go and verify compatibility?"If the user confirms, execute the following steps in order:
1. Backup — copy the current generated type output directory (e.g. @mf-types/) to a timestamped backup path such as @mf-types.bak.<timestamp>/
2. Configure — set dts.generateTypes.compilerInstance = "tsgo" in the Module Federation config
3. Install — install the required package using the project's package manager from MFContext:
pnpm add @typescript/native-preview --save-dev4. Regenerate — run:
npx mf dts5. Verify — diff the newly generated type output against the backup:
- If the output is identical: inform the user that
ts-gois compatible and the switch is safe; offer to remove the backup - If the output differs: revert the config change, restore the backup, and explain clearly what differs (e.g. missing declarations, changed signatures) so the user can decide whether the difference is acceptable
Webpack projects do not show Rspack-specific entries to avoid irrelevant suggestions.
Sub-skill: runtime-error
Diagnose explicit Module Federation runtime error codes.
This sub-skill is only for users who already have a clear runtime error code such as RUNTIME-001 or RUNTIME-008.
If ARGS includes an observability report, traceId, console read: command, or .mf/observability file path, stop here and follow ./observability.md instead.
Runtime error codes remain useful even when the observability plugin is not enabled. Do not tell the user that diagnosis requires @module-federation/observability-plugin. Use the error code, console text, network evidence, and runtime/build config first. Recommend the observability plugin only as an optional way to collect richer evidence when the code alone is not enough. The observability plugin is designed for Module Federation 2.5.0+; when recommending it, ask whether the user can upgrade MF to 2.5.0 or later and enable the plugin.
If ARGS only says that Module Federation failed, but does not include an accurate runtime error code, observability report, useful console details, network evidence, or config evidence, ask the user whether they can follow the recommended path: upgrade to Module Federation 2.5.0+ and use @module-federation/observability-plugin to collect an actionable report.
Step 1: Parse the runtime code first
Extract the runtime code from ARGS.
- If the code is
RUNTIME-001orRUNTIME-008, continue with this sub-skill - If the code is any other
RUNTIME-xxx, do not continue local diagnosis here. Instead, read and follow./docs.mdand look up the corresponding runtime error code in the official troubleshooting docs - If there is no clear runtime code, ask the user for the exact code before proceeding
Step 2: Special handling for RUNTIME-001 and RUNTIME-008
These two codes need special handling because in versions before `2.3.0`, some remote entry failures may be reported imprecisely:
- some real
runtime-008cases may appear asruntime-001 - older versions may hide the original browser exception detail
The goal is to confirm whether the real problem is:
1. ScriptNetworkError — the remote entry could not be downloaded 2. ScriptExecutionError — the remote entry downloaded, but threw during execution 3. legacy hidden execution error — old versions reported it as runtime-001 / vague runtime-008 because the browser detail was hidden
Step 3: Prefer automatic browser capture
Prefer using the built-in browser capture helper inside mf to capture browser evidence automatically before asking the user to paste logs manually.
3a. Check whether Chrome remote debugging is available
Run:
curl -s http://localhost:9222/json/version- If reachable: continue with automatic capture
- If not reachable: follow
./browser-debug/setup.md
3b. Capture the failing page
Use ../scripts/browser-capture.mjs against the failing page URL.
Prefer this baseline command first:
node ../scripts/browser-capture.mjs "<failing-page-url>" 20000 --vars __FEDERATION__,__webpack_require__,window.__FEDERATION__If the page is noisy or polling heavily, prefer:
node ../scripts/browser-capture.mjs "<failing-page-url>" 15000 --vars __FEDERATION__,window.__FEDERATION__ --no-entriesIf the error only happens after user interaction, follow ./browser-debug/long-chain.md.
Step 4: Fallback when auto capture is unavailable
If automatic capture cannot be used, ask the user to provide:
1. The exact browser console error text 2. The failing remoteEntry or manifest URL 3. Whether opening that URL directly in the browser returns the file successfully 4. Whether the Network panel shows a failed request, and the status code 5. Whether the console contains the original exception text (TypeError, SyntaxError, etc.)
Step 5: Classify the result for 001 / 008
Case A — ScriptNetworkError
Treat it as network-layer load failure when any of the following is true:
- browser capture shows network failure, CORS failure, timeout, DNS failure, or 4xx/5xx
- opening the remote entry URL directly fails
- the manifest
publicPath/remoteEntrypoints to the wrong address
Then guide the user to check:
1. whether the URL is correct 2. whether the resource is externally reachable 3. whether CORS is configured correctly 4. whether CDN or gateway routing is broken
Case B — ScriptExecutionError
Treat it as download succeeded but execution failed when:
- the remote entry request succeeds
- browser capture shows a JS exception during script execution
- the error contains original exception details such as
TypeErrororSyntaxError
Then guide the user to check:
1. browser compatibility of the producer build target 2. whether the producer entry relies on globals that are not initialized 3. whether the build artifact is incomplete or corrupted 4. the exact failing line from the browser exception
Retries do not fix this case.
Case C — legacy hidden execution error
Treat it as a likely hidden execution error when:
- the remote entry request succeeded
window[remoteEntryKey]is missing after the script loads- the browser console does not show a useful exception, or only shows a vague old runtime code
- the project version is before
2.3.0, or the user explicitly says the message lacks detail
Then explain:
- this may be an old-version reporting limitation rather than a true "URL is wrong" problem
- the real cause is often still a script execution failure, but the old runtime did not expose enough browser detail
Step 6: Recover more detail for legacy cases
For the legacy hidden-execution case, follow the same documentation approach used for RUNTIME-001:
1. add a runtime plugin with createScript that sets crossOrigin = 'anonymous' 2. if preload is used, also set crossorigin in createLink 3. ensure the producer serves proper Access-Control-Allow-Origin 4. ensure producer build output sets output.crossOriginLoading = 'anonymous'
Use this only after confirming the server already supports CORS. Otherwise the script may fail to load entirely.
Step 7: Final conclusion template
At the end, clearly state:
1. whether the issue is:
ScriptNetworkErrorScriptExecutionError- legacy hidden execution error
2. what concrete evidence supports the conclusion 3. what the next fix should be 4. that 2.3.0+ improves raw runtime error categories, and the recommended observability path is upgrading to `2.5.0+` and enabling @module-federation/observability-plugin
Required final reminder
Always end with this recommendation in plain language:
- older versions may report this kind of problem with incomplete or misleading runtime codes
- upgrading to `2.5.0+` and enabling
@module-federation/observability-plugingives a fuller loading report when the raw error code is not enough
Sub-skill: shared-deps
Check Module Federation shared dependency configuration: detect shared/externals conflicts, antd/arco transformImport blocking shared deps, and multiple versions of the same shared package in build artifacts.
Step 1: Collect MFContext
Read and follow the instructions in ./context.md, passing ARGS as the project root.
Step 2: Run shared config check script
Serialize MFContext to JSON and pass it to the check script:
node scripts/shared-config-check.js --context '<MFContext-JSON>'Process each item in the output results array:
SHARED-EXTERNALS-CONFLICT · warning — same library in both `shared` and `externals`
sharedandexternalsare not mutually exclusive in config, but the same library must not appear in both — it causes the module to be excluded from the bundle while also being declared as shared, leading to runtime failures- Show the conflicting library name and guide the user to remove it from one of the two configs
SHARED-TRANSFORM-IMPORT · warning — antd/arco UI library shared but `transformImport` is active
babel-plugin-import(or the built-intransformImportin Modern.js / Rsbuild) rewrites import paths at build time, which prevents the shared dep from being recognized and causes sharing to fail silently- Fix:
- Modern.js / Rsbuild: set
source.transformImport = falseto disable the built-in behavior - Other bundlers: remove
babel-plugin-importfrom the Babel config - Show which UI library triggered the warning
SHARED-MULTI-VERSION · warning — multiple versions of the same shared package detected
- The build artifacts contain more than one version of a shared package, meaning the version negotiation failed and both host and remote are each bundling their own copy
- Recommended fix: add an
aliasin the bundler config so all projects resolve to the same physical file - Show the detected versions
When results is empty
- Inform the user that no shared dependency conflicts were detected in this project
- Remind them that a complete picture requires running the same check in both the host and every remote
Sub-skill: type-check
Diagnose Module Federation type issues across three categories: 1. Producer type file generation failures (TYPE-001) 2. Consumer failing to pull remote types 3. tsconfig not configured to consume remote types
Step 1: Collect MFContext
Read and follow the instructions in ./context.md, passing ARGS as the project root.
Step 2: Run type check script
Serialize MFContext to JSON and pass it to the check script:
node scripts/type-check.js --context '<MFContext-JSON>'Process each item in the output results array and follow the action plan based on the scenario field:
---
Scenario: TYPE_GENERATION_FAILED (Problem 1 — Producer type files not generated)
The producer failed to generate type files (TYPE-001 error).
If `enhancedVersion` > `2.0.1` (result field canReadDiagnostics: true): 1. Read .mf/observability/latest.json to get full error info and the temporary TS config path 2. Use the temp TS config path with npx tsc --project <tmp-tsconfig> to reproduce errors 3. Fix the TS errors revealed. Refer to FAQ: https://module-federation.io/guide/troubleshooting/type.md 4. Offer "skipLibCheck": true as a temporary workaround if errors are complex
If `enhancedVersion` <= `2.0.1` (result field canReadDiagnostics: false): 1. Ask the user to run npx mf dts and paste the terminal output (which includes the temp TS config path) 2. Or ask them to copy the error message that contains the temp TS config path 3. Once the temp TS config path is known, run npx tsc --project <tmp-tsconfig> to reproduce and fix errors 4. Offer "skipLibCheck": true as a temporary workaround
---
Scenario: TYPES_NOT_PULLED (Problem 2 — Consumer not pulling remote types)
The @mf-types folder is missing. Remote types have not been downloaded.
1. Read and follow ./module-info.md with the remote module name to retrieve the type file URL (@mf-types.zip)
- If no URL returned: the producer has not configured the type file URL or has not generated types. Guide them to enable
dtsin the@module-federation/enhancedplugin config, then revisit Problem 1 - If URL found: attempt to fetch it (or ask the user to verify in browser)
- URL inaccessible: try fetching the
remoteEntryURL remoteEntryunreachable: producer deployment is broken or URL is misconfigured; ask user to verify deploymentremoteEntryreachable: type file generation failed or wasn't deployed; ask user to provide local producer path and proceed to Problem 1- URL accessible: types were generated and deployed; the issue is in tsconfig — proceed to Problem 3
---
Scenario: TSCONFIG_PATHS_MISSING (Problem 3 — tsconfig not configured for remote types)
The @mf-types folder exists but TypeScript cannot find the types because tsconfig.json is missing the paths mapping.
1. Open tsconfig.json and add the following to compilerOptions.paths:
{
"compilerOptions": {
"paths": {
"*": ["./@mf-types/*"]
}
}
}2. If paths already exists, merge the new entry without overwriting existing mappings 3. After updating, run npx tsc --noEmit to verify the type errors are resolved
---
Scenario: ENV_INCOMPLETE (Missing tsconfig or TypeScript)
TYPE-001 · warning — `tsconfig.json` missing
tsconfig.jsonnot found in the project root- Advise the user to create
tsconfig.jsonand configure producer type paths inpaths
TYPE-001 · warning — `typescript` dependency missing
typescriptnot installed independencies/devDependencies- Prompt the user to install:
pnpm add -D typescript
---
This sub-skill performs configuration and dependency-level checks. It runsnpx tsconly when guided by a valid temp TS config path. It never runstscblindly against the entire project.
#!/usr/bin/env node
function parseArgs(argv) {
const args = {};
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
if (arg.startsWith('--')) {
const eqIdx = arg.indexOf('=');
if (eqIdx >= 0) {
args[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
} else {
args[arg.slice(2)] = argv[i + 1] || '';
i++;
}
}
}
return args;
}
function main(ctx) {
const results = [];
if (ctx.mfConfig && ctx.mfConfig.exposes) {
const hasExportApp = Object.keys(ctx.mfConfig.exposes).some((k) =>
/export-app/i.test(k),
);
if (!hasExportApp) {
results.push({
code: 'BRIDGE-USAGE',
severity: 'info',
message:
'No obvious export-app export found in exposes. Please verify that the producer exports the app entry following the Bridge spec.',
context: { exposesKeys: Object.keys(ctx.mfConfig.exposes) },
});
}
}
results.push({
code: 'BRIDGE-USAGE',
severity: 'info',
message:
'On the consumer side, prefer using official APIs such as createRemoteAppComponent instead of directly concatenating remote URLs.',
context: {},
});
process.stdout.write(
`${JSON.stringify({ context: ctx, results }, null, 2)}\n`,
);
}
const args = parseArgs(process.argv);
main(JSON.parse(args.context));
#!/usr/bin/env node
// capture.mjs — collect browser logs + JS variables via Chrome DevTools Protocol
//
// New tab: node capture.mjs <url> [timeout_ms] [--vars v1,v2] [--keep-tab] [--click "text"] [--dump-dom] [--action-wait auto|networkidle|domcontentloaded|timeout|none] [--no-entries]
// Existing tab: node capture.mjs --tab-id <id> [--click "text"] [--fill "ph::text"] [--select "ph::value"] [--vars v1,v2] [--dump-dom] [--close] [--action-wait auto|networkidle|domcontentloaded|timeout|none]
//
// Long-chain example:
// TAB=$(node capture.mjs https://example.com --keep-tab | jq -r .tabId)
// node capture.mjs --tab-id $TAB --click "个人"
// node capture.mjs --tab-id $TAB --fill "搜索框placeholder::关键词"
// node capture.mjs --tab-id $TAB --select "请选择::选项A"
// node capture.mjs --tab-id $TAB --click "添加" --vars __FEDERATION__ --close
const CDP_BASE = 'http://localhost:9222';
const IDLE_MS = 500;
// ── argument parsing ──────────────────────────────────────────────────────────
const args = process.argv.slice(2);
function flagVal(flag) {
const i = args.indexOf(flag);
return i !== -1 ? args[i + 1] : null;
}
const tabId = flagVal('--tab-id');
const clickTarget = flagVal('--click');
const fillArg = flagVal('--fill'); // "placeholder::text"
const selectArg = flagVal('--select'); // "placeholder::value"
const varNamesRaw = flagVal('--vars');
const varNames = varNamesRaw
? varNamesRaw
.split(',')
.map((s) => s.trim())
.filter(Boolean)
: [];
const evalExpr = flagVal('--eval');
const keepTab = args.includes('--keep-tab');
const closeTab = args.includes('--close');
const dumpDom = args.includes('--dump-dom');
const noEntriesFlag = args.includes('--no-entries');
const entriesLimitRaw = flagVal('--entries-limit');
const entriesLimit = entriesLimitRaw ? Number(entriesLimitRaw) : null;
const waitUntilRaw = flagVal('--wait-until');
const actionWaitRaw = flagVal('--action-wait');
const hasWaitUntilFlag = waitUntilRaw != null;
const hasActionWaitFlag = actionWaitRaw != null;
const parsedWaitUntil =
waitUntilRaw === 'domcontentloaded' ||
waitUntilRaw === 'networkidle' ||
waitUntilRaw === 'timeout' ||
waitUntilRaw === 'auto'
? waitUntilRaw
: null;
const parsedActionWait =
actionWaitRaw === 'networkidle' ||
actionWaitRaw === 'domcontentloaded' ||
actionWaitRaw === 'timeout' ||
actionWaitRaw === 'none' ||
actionWaitRaw === 'auto'
? actionWaitRaw
: null;
const disableEarlyStop = args.includes('--disable-early-stop');
const followNewTab = !args.includes('--no-follow-new-tab');
// positional args: skip flag names and their values
const flagsWithValues = new Set([
'--tab-id',
'--click',
'--fill',
'--select',
'--vars',
'--eval',
'--wait-until',
'--action-wait',
'--entries-limit',
]);
const skipIdx = new Set();
args.forEach((a, i) => {
if (flagsWithValues.has(a)) {
skipIdx.add(i);
skipIdx.add(i + 1);
}
});
const positional = args.filter(
(a, i) => !a.startsWith('--') && !skipIdx.has(i),
);
const targetUrl = positional[0] ?? null;
const timeout = Number(positional[1] ?? 15_000);
if (!targetUrl && !tabId) {
process.stderr.write(
'Usage:\n' +
' node capture.mjs <url> [timeout_ms] [--vars v1,v2] [--keep-tab] [--click "text"] [--dump-dom] [--eval "expr"] [--wait-until auto|domcontentloaded|networkidle|timeout] [--action-wait auto|networkidle|domcontentloaded|timeout|none] [--no-entries] [--entries-limit N]\n' +
' node capture.mjs --tab-id <id> [--click "text"] [--vars v1,v2] [--dump-dom] [--close] [--eval "expr"] [--action-wait auto|networkidle|domcontentloaded|timeout|none] [--no-follow-new-tab]\n',
);
process.exit(1);
}
if (!Number.isFinite(timeout) || timeout <= 0) {
process.stderr.write(
`Invalid timeout: ${String(positional[1])}. timeout_ms must be a positive number.\n`,
);
process.exit(1);
}
if (
entriesLimitRaw != null &&
(!Number.isInteger(entriesLimit) || entriesLimit <= 0)
) {
process.stderr.write(
`Invalid --entries-limit: ${entriesLimitRaw}. It must be a positive integer.\n`,
);
process.exit(1);
}
if (waitUntilRaw != null && parsedWaitUntil == null) {
process.stderr.write(
`Invalid --wait-until: ${waitUntilRaw}. Valid values: auto, domcontentloaded, networkidle, timeout.\n`,
);
process.exit(1);
}
if (actionWaitRaw != null && parsedActionWait == null) {
process.stderr.write(
`Invalid --action-wait: ${actionWaitRaw}. Valid values: auto, networkidle, domcontentloaded, timeout, none.\n`,
);
process.exit(1);
}
const AUTO_NETWORKIDLE_MAX_MS = 6_000;
const hasInteractionAction = Boolean(clickTarget || fillArg || selectArg);
const hasPostActionCapture = Boolean(evalExpr || varNames.length || dumpDom);
// Auto-enable --no-entries for pure interaction steps on an existing tab (click/fill/select
// with no variable/eval/dom capture): entries are noise and slow down the chain.
const noEntries =
noEntriesFlag ||
Boolean(tabId && hasInteractionAction && !hasPostActionCapture);
const waitUntil = parsedWaitUntil ?? 'auto';
const actionWait = parsedActionWait ?? 'auto';
const effectiveWaitUntil =
waitUntil === 'auto' ? 'domcontentloaded' : waitUntil;
const effectiveActionWait =
actionWait === 'auto'
? hasInteractionAction
? 'domcontentloaded'
: 'none'
: actionWait;
const navigateWaitBudgetMs = hasWaitUntilFlag
? timeout
: Math.min(timeout, AUTO_NETWORKIDLE_MAX_MS);
const actionWaitBudgetMs = hasActionWaitFlag
? timeout
: Math.min(timeout, AUTO_NETWORKIDLE_MAX_MS);
if (typeof WebSocket === 'undefined') {
process.stderr.write(
'Node.js 21+ required (built-in WebSocket). Current: ' +
process.version +
'\n',
);
process.exit(1);
}
// ── CDP session ───────────────────────────────────────────────────────────────
class Session {
#ws;
#nextId = 1;
#pending = new Map();
#listeners = new Map();
constructor(wsUrl) {
this.#ws = new WebSocket(wsUrl);
this.#ws.addEventListener('message', ({ data }) => {
const msg = JSON.parse(data);
if (msg.id != null) {
const p = this.#pending.get(msg.id);
this.#pending.delete(msg.id);
msg.error
? p?.reject(new Error(msg.error.message))
: p?.resolve(msg.result);
}
if (msg.method)
this.#listeners.get(msg.method)?.forEach((fn) => fn(msg.params));
});
}
open() {
return new Promise((resolve, reject) => {
this.#ws.addEventListener('open', resolve, { once: true });
this.#ws.addEventListener(
'error',
(e) => reject(new Error(String(e.message ?? e))),
{ once: true },
);
});
}
send(method, params = {}) {
const id = this.#nextId++;
this.#ws.send(JSON.stringify({ id, method, params }));
return new Promise((resolve, reject) =>
this.#pending.set(id, { resolve, reject }),
);
}
on(event, fn) {
if (!this.#listeners.has(event)) this.#listeners.set(event, []);
this.#listeners.get(event).push(fn);
}
close() {
this.#ws.close();
}
}
// ── verify Chrome reachable ───────────────────────────────────────────────────
try {
await fetch(`${CDP_BASE}/json/version`);
} catch {
process.stderr.write(
'Cannot reach Chrome on port 9222.\n\n' +
'Quit Chrome and relaunch with remote debugging (macOS):\n' +
' CHROME=$(find /Applications ~/Applications -name "Google Chrome" -path "*/MacOS/Google Chrome" 2>/dev/null | head -1)\n' +
' killall "Google Chrome" 2>/dev/null; sleep 1\n' +
' "$CHROME" --remote-debugging-port=9222 --user-data-dir="$HOME/Library/Application Support/Google/Chrome" &\n\n' +
'This uses your REAL Chrome profile — all cookies and login sessions are preserved.\n',
);
process.exit(1);
}
// ── get or create tab ─────────────────────────────────────────────────────────
let tab;
if (tabId) {
const tabs = await (await fetch(`${CDP_BASE}/json/list`)).json();
tab = tabs.find((t) => t.id === tabId);
if (!tab) {
process.stderr.write(`Tab not found: ${tabId}\nActive tabs:\n`);
tabs.forEach((t) => process.stderr.write(` ${t.id} ${t.url}\n`));
process.exit(1);
}
process.stderr.write(`Attaching to tab: ${tab.url}\n`);
} else {
process.stderr.write(
`Navigating to ${targetUrl} (timeout: ${timeout / 1000}s)...\n`,
);
tab = await (await fetch(`${CDP_BASE}/json/new`, { method: 'PUT' })).json();
}
let session = new Session(tab.webSocketDebuggerUrl);
await session.open();
// ── log collection ────────────────────────────────────────────────────────────
const logs = [];
const stamp = () => new Date().toISOString();
session.on('Runtime.consoleAPICalled', ({ type, args: a, stackTrace }) => {
const msg = a
.map((x) =>
x.type === 'string'
? x.value
: x.description
? x.description
: x.value != null
? String(x.value)
: x.type,
)
.join(' ');
const f = stackTrace?.callFrames?.[0];
logs.push({
t: stamp(),
level: type === 'warning' ? 'warn' : type,
msg,
stack: f ? `${f.url}:${f.lineNumber + 1}:${f.columnNumber + 1}` : null,
});
});
session.on('Runtime.exceptionThrown', ({ exceptionDetails: ex }) => {
const msg = ex.exception?.description ?? ex.text ?? 'Unknown exception';
const f = ex.stackTrace?.callFrames?.[0];
logs.push({
t: stamp(),
level: 'error',
msg,
stack: f ? `${f.url}:${f.lineNumber + 1}` : null,
});
});
session.on('Network.responseReceived', ({ response }) => {
if (response.status < 400) return;
logs.push({
t: stamp(),
level: response.status >= 500 ? 'error' : 'warn',
msg: `[HTTP] ${response.status} ${response.statusText} — ${response.url}`,
stack: null,
});
});
const pendingUrls = new Map();
session.on('Network.requestWillBeSent', ({ requestId, request }) =>
pendingUrls.set(requestId, request.url),
);
session.on(
'Network.loadingFailed',
({ requestId, errorText, blockedReason, canceled }) => {
if (canceled) return;
logs.push({
t: stamp(),
level: 'error',
msg: `[network] ${blockedReason ?? errorText} — ${pendingUrls.get(requestId) ?? '?'}`,
stack: null,
});
pendingUrls.delete(requestId);
},
);
session.on('Log.entryAdded', ({ entry }) => {
if (entry.level === 'verbose') return;
logs.push({
t: stamp(),
level: entry.level === 'warning' ? 'warn' : entry.level,
msg: entry.text,
stack: entry.url ? `${entry.url}:${entry.lineNumber ?? 0}` : null,
});
});
// ── reusable network-idle waiter ──────────────────────────────────────────────
let inflight = 0;
let idleTimer = null;
const idleCallbacks = new Set();
let lastWait = { earlyStop: false, timedOut: false };
function fireIdle() {
const cbs = [...idleCallbacks];
idleCallbacks.clear();
cbs.forEach((cb) => cb());
}
function scheduleIdle() {
if (inflight === 0 && idleCallbacks.size > 0) {
clearTimeout(idleTimer);
idleTimer = setTimeout(fireIdle, IDLE_MS);
}
}
session.on('Network.requestWillBeSent', () => {
inflight++;
clearTimeout(idleTimer);
idleTimer = null;
});
session.on('Network.loadingFinished', () => {
inflight = Math.max(0, inflight - 1);
scheduleIdle();
});
session.on('Network.loadingFailed', () => {
inflight = Math.max(0, inflight - 1);
scheduleIdle();
});
function waitForNetworkIdle(maxMs = timeout) {
return new Promise((resolve) => {
let done = false;
const cb = () => {
if (done) return;
done = true;
lastWait = { earlyStop: true, timedOut: false };
resolve();
};
idleCallbacks.add(cb);
scheduleIdle();
setTimeout(() => {
if (done) return;
done = true;
lastWait = { earlyStop: false, timedOut: true };
resolve();
}, maxMs);
});
}
async function waitAfterAction(mode, maxMs = timeout) {
if (mode === 'none') {
lastWait = { earlyStop: true, timedOut: false };
return;
}
if (mode === 'networkidle') {
await waitForNetworkIdle(maxMs);
return;
}
if (mode === 'timeout') {
await new Promise((r) => setTimeout(r, maxMs));
lastWait = { earlyStop: false, timedOut: true };
return;
}
await new Promise((r) => setTimeout(r, Math.min(500, maxMs)));
lastWait = { earlyStop: true, timedOut: false };
}
// ── enable domains & navigate ─────────────────────────────────────────────────
await Promise.all([
session.send('Runtime.enable'),
session.send('Network.enable'),
session.send('Log.enable'),
session.send('Page.enable'),
]);
const timings = {
navigateMs: null,
clickMs: null,
fillMs: null,
selectMs: null,
evalMs: null,
varsMs: null,
totalMs: null,
};
let tabTransition = null;
const tStart = Date.now();
if (!tabId && targetUrl) {
const navigateStart = Date.now();
const pageLoaded = new Promise((r) => session.on('Page.loadEventFired', r));
await session.send('Page.navigate', { url: targetUrl });
await pageLoaded;
if (!disableEarlyStop && effectiveWaitUntil === 'networkidle') {
await waitForNetworkIdle(navigateWaitBudgetMs);
} else if (effectiveWaitUntil === 'domcontentloaded') {
// do nothing extra
} else {
// timeout-only
await new Promise((r) => setTimeout(r, timeout));
lastWait = { earlyStop: false, timedOut: true };
}
timings.navigateMs = Date.now() - navigateStart;
}
// ── click element ─────────────────────────────────────────────────────────────
let clickResult = null;
if (clickTarget) {
const clickStart = Date.now();
process.stderr.write(`Clicking: \"${clickTarget}\"\\n`);
const beforeTabs = followNewTab
? await (await fetch(`${CDP_BASE}/json/list`)).json()
: null;
const r = await session.send('Runtime.evaluate', {
expression: `(function(q) {
const toText = (node) => (node?.textContent || '').trim();
const byText = (nodes) => {
const exact = nodes.find(n => toText(n) === q);
if (exact) return { el: exact, matchType: 'exact' };
const prefix = nodes.find(n => toText(n).startsWith(q));
if (prefix) return { el: prefix, matchType: 'prefix' };
const contains = nodes.find(n => toText(n).includes(q));
if (contains) return { el: contains, matchType: 'contains' };
return null;
};
const isSelector = q.startsWith('#') || q.startsWith('.') || q.startsWith('[') || q.includes('>');
let el = null;
let matchStrategy = 'none';
let matchType = 'none';
if (isSelector) {
try {
el = document.querySelector(q);
if (el) {
matchStrategy = 'css';
matchType = 'selector';
}
} catch(e) {}
}
if (!el) {
const strongCandidates = Array.from(document.querySelectorAll(
'button, a, [role=button], [role=tab], [role=menuitem], [role=option], input[type=button], input[type=submit]'
));
const hit = byText(strongCandidates);
if (hit) {
el = hit.el;
matchStrategy = 'interactive';
matchType = hit.matchType;
}
}
if (!el) {
const weakCandidates = Array.from(document.querySelectorAll('div, span, li')).filter(node => {
const hasOnclick = typeof node.onclick === 'function';
const tabIndex = node.getAttribute('tabindex');
const focusable = tabIndex !== null && Number(tabIndex) >= 0;
let pointer = false;
try { pointer = window.getComputedStyle(node).cursor === 'pointer'; } catch(e) {}
return hasOnclick || focusable || pointer;
});
const hit = byText(weakCandidates);
if (hit) {
el = hit.el;
matchStrategy = 'weak-interactive';
matchType = hit.matchType;
}
}
if (!el) return JSON.stringify({ found: false, tried: q, matchStrategy: 'none' });
el.scrollIntoView({ behavior: 'instant', block: 'center' });
el.click();
return JSON.stringify({
found: true,
tag: el.tagName.toLowerCase(),
text: toText(el).slice(0, 80),
id: el.id || null,
className: typeof el.className === 'string' ? el.className : null,
matchStrategy,
matchType,
});
})(${JSON.stringify(clickTarget)})`,
returnByValue: true,
});
clickResult = JSON.parse(r?.result?.value ?? '{\"found\":false}');
if (!clickResult.found) {
process.stderr.write(
` Warning: element not found for \"${clickTarget}\"\\n`,
);
} else {
process.stderr.write(
` Clicked: <${clickResult.tag}> \"${clickResult.text}\" (${clickResult.matchStrategy}/${clickResult.matchType})\\n`,
);
// wait briefly for click-triggered requests to start, then wait by action mode
await new Promise((r) => setTimeout(r, 200));
await waitAfterAction(effectiveActionWait, actionWaitBudgetMs);
// follow new tab if opened
if (followNewTab && beforeTabs) {
const beforeIds = new Set(beforeTabs.map((t) => t.id));
let newTarget = null;
const deadline = Date.now() + 3000;
while (Date.now() < deadline && !newTarget) {
const now = await (await fetch(`${CDP_BASE}/json/list`)).json();
newTarget = now.find(
(t) => !beforeIds.has(t.id) && (t.type === 'page' || !t.type),
);
if (!newTarget) await new Promise((r) => setTimeout(r, 250));
}
if (newTarget) {
// switch session to new tab
const fromTabId = tab.id;
const toTabId = newTarget.id;
session.close();
tab = newTarget;
session = new Session(tab.webSocketDebuggerUrl);
await session.open();
await Promise.all([
session.send('Runtime.enable'),
session.send('Network.enable'),
session.send('Log.enable'),
session.send('Page.enable'),
]);
const readyState = await session.send('Runtime.evaluate', {
expression: 'document.readyState',
returnByValue: true,
});
tabTransition = {
fromTabId,
toTabId,
reason: 'new-tab-after-click',
readyState: readyState?.result?.value ?? null,
};
// annotate click result with tab switch
clickResult.switchedToNewTab = true;
clickResult.fromTabId = fromTabId;
clickResult.toTabId = toTabId;
}
}
}
timings.clickMs = Date.now() - clickStart;
}
// ── fill input (locate by placeholder) ───────────────────────────────────────
let fillResult = null;
if (fillArg) {
const fillStart = Date.now();
const sep = fillArg.indexOf('::');
const placeholder = sep !== -1 ? fillArg.slice(0, sep) : fillArg;
const text = sep !== -1 ? fillArg.slice(sep + 2) : '';
process.stderr.write(
`Filling: placeholder="${placeholder}" text="${text}"\n`,
);
const r = await session.send('Runtime.evaluate', {
expression: `(function(ph, txt) {
const el = document.querySelector('input[placeholder="' + ph + '"], textarea[placeholder="' + ph + '"]');
if (!el) return JSON.stringify({ found: false, tried: ph });
el.focus();
el.scrollIntoView({ behavior: 'instant', block: 'center' });
// React/Vue-compatible: use native setter to trigger synthetic events
const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
setter.call(el, txt);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
return JSON.stringify({ found: true, tag: el.tagName.toLowerCase(), placeholder: el.placeholder });
})(${JSON.stringify(placeholder)}, ${JSON.stringify(text)})`,
returnByValue: true,
});
fillResult = JSON.parse(r?.result?.value ?? '{"found":false}');
if (!fillResult.found) {
process.stderr.write(
` Warning: input not found for placeholder="${placeholder}"\n`,
);
} else {
process.stderr.write(
` Filled: <${fillResult.tag}> placeholder="${fillResult.placeholder}"\n`,
);
await new Promise((r) => setTimeout(r, 200));
await waitAfterAction(effectiveActionWait, actionWaitBudgetMs);
}
timings.fillMs = Date.now() - fillStart;
}
// ── select option (locate by placeholder) ────────────────────────────────────
let selectResult = null;
if (selectArg) {
const selectStart = Date.now();
const sep = selectArg.indexOf('::');
const placeholder = sep !== -1 ? selectArg.slice(0, sep) : selectArg;
const value = sep !== -1 ? selectArg.slice(sep + 2) : '';
process.stderr.write(
`Selecting: placeholder="${placeholder}" value="${value}"\n`,
);
// Step 1: try native <select>, otherwise click the custom dropdown trigger
const r1 = await session.send('Runtime.evaluate', {
expression: `(function(ph, val) {
// native <select>: match by placeholder attr or first option text
const selects = Array.from(document.querySelectorAll('select'));
const nativeSel = selects.find(s =>
s.getAttribute('placeholder') === ph ||
(s.options[0] && s.options[0].text.trim() === ph)
);
if (nativeSel) {
const opt = Array.from(nativeSel.options).find(o => o.text.trim() === val || o.value === val);
if (!opt) return JSON.stringify({ found: false, reason: 'option not found', tried: val });
nativeSel.value = opt.value;
nativeSel.dispatchEvent(new Event('change', { bubbles: true }));
return JSON.stringify({ found: true, type: 'native', value: opt.value, text: opt.text.trim() });
}
// custom dropdown: find trigger by placeholder attr or visible placeholder text
let trigger = document.querySelector('[placeholder="' + ph + '"]');
if (!trigger) {
const candidates = Array.from(document.querySelectorAll(
'[role=combobox], [aria-haspopup], [class*=select], [class*=dropdown]'
));
trigger = candidates.find(e => e.textContent.trim() === ph);
}
if (!trigger) return JSON.stringify({ found: false, reason: 'trigger not found', tried: ph });
trigger.scrollIntoView({ behavior: 'instant', block: 'center' });
trigger.click();
return JSON.stringify({ found: true, type: 'custom', step: 'trigger_clicked' });
})(${JSON.stringify(placeholder)}, ${JSON.stringify(value)})`,
returnByValue: true,
});
selectResult = JSON.parse(r1?.result?.value ?? '{"found":false}');
if (
selectResult.type === 'custom' &&
selectResult.step === 'trigger_clicked'
) {
// Step 2: wait for dropdown to open, then click the matching option
await new Promise((r) => setTimeout(r, 300));
const r2 = await session.send('Runtime.evaluate', {
expression: `(function(val) {
const opts = Array.from(document.querySelectorAll(
'option, [role=option], [role=menuitem], [class*=option-item], [class*=dropdown-item]'
));
const opt = opts.find(e => e.textContent.trim() === val)
?? opts.find(e => e.textContent.trim().includes(val));
if (!opt) return JSON.stringify({ found: false, tried: val });
opt.click();
return JSON.stringify({ found: true, type: 'custom', text: opt.textContent.trim() });
})(${JSON.stringify(value)})`,
returnByValue: true,
});
selectResult = JSON.parse(r2?.result?.value ?? '{"found":false}');
}
if (!selectResult.found) {
process.stderr.write(
` Warning: select target not found (${selectResult.reason ?? 'unknown'})\n`,
);
} else {
process.stderr.write(
` Selected: [${selectResult.type}] "${selectResult.text ?? selectResult.value}"\n`,
);
await new Promise((r) => setTimeout(r, 200));
await waitAfterAction(effectiveActionWait, actionWaitBudgetMs);
}
timings.selectMs = Date.now() - selectStart;
}
// ── DOM dump (for Claude to identify selectors) ───────────────────────────────
let domSnapshot = null;
if (dumpDom) {
const r = await session.send('Runtime.evaluate', {
expression: `(function() {
function walk(el, depth) {
if (depth > 4) return null;
const tag = el.tagName?.toLowerCase();
if (!tag || ['script','style','svg','noscript','head'].includes(tag)) return null;
const text = el.children.length === 0 ? el.textContent.trim().slice(0, 120) : '';
const attrs = {};
if (el.id) attrs.id = el.id;
if (el.className && typeof el.className === 'string') {
const cls = el.className.split(' ').filter(Boolean);
if (cls.length) attrs.class = cls.slice(0, 4).join(' ');
}
const role = el.getAttribute('role'); if (role) attrs.role = role;
const children = Array.from(el.children).map(c => walk(c, depth + 1)).filter(Boolean);
if (!text && !children.length && !Object.keys(attrs).length) return null;
return { tag, ...(Object.keys(attrs).length ? { attrs } : {}), ...(text ? { text } : {}), ...(children.length ? { children } : {}) };
}
return JSON.stringify(walk(document.body, 0));
})()`,
returnByValue: true,
});
domSnapshot = JSON.parse(r?.result?.value ?? 'null');
}
// ── variable capture / eval ───────────────────────────────────────────────────
let evalResult = null;
if (evalExpr) {
const evalStart = Date.now();
process.stderr.write(`Evaluating expression: ${evalExpr}\n`);
try {
const r = await session.send('Runtime.evaluate', {
expression: evalExpr,
returnByValue: true,
});
evalResult = r?.result?.value ?? null;
} catch (e) {
evalResult = { error: String(e.message) };
}
timings.evalMs = Date.now() - evalStart;
}
const variables = {};
if (varNames.length) {
const varsStart = Date.now();
process.stderr.write(`Capturing variables: ${varNames.join(', ')}\n`);
const safeSerializerSrc = `
(function captureVar(pathExpr) {
const skipped = [];
const seen = new WeakMap();
function safe(val, path, depth) {
if (depth > 5) { skipped.push({ path, reason: 'max_depth' }); return '[max depth]'; }
if (val === null || val === undefined) return val;
const t = typeof val;
if (t === 'boolean' || t === 'number' || t === 'string') return val;
if (t === 'bigint') return val.toString() + 'n';
if (t === 'symbol') return val.toString();
if (t === 'function') {
skipped.push({ path, reason: 'function', detail: val.name || 'anonymous' });
return '[Function: ' + (val.name || 'anonymous') + ']';
}
if (seen.has(val)) {
skipped.push({ path, reason: 'circular', circularRef: seen.get(val) });
return '[Circular -> ' + seen.get(val) + ']';
}
seen.set(val, path);
if (Array.isArray(val)) return val.map((v, i) => safe(v, path + '[' + i + ']', depth + 1));
const obj = {};
for (const k of Object.keys(val)) {
try { obj[k] = safe(val[k], path + '.' + k, depth + 1); }
catch (e) { skipped.push({ path: path + '.' + k, reason: 'error', detail: e.message }); obj[k] = '[Error: ' + e.message + ']'; }
}
return obj;
}
function normalizePath(path) {
return path
.replace(/\[(\d+)\]/g, '.$1')
.replace(/\[['\"]([^'\"]+)['\"]\]/g, '.$1');
}
function getByPath(root, path) {
if (Object.prototype.hasOwnProperty.call(root, path)) return root[path];
const normalized = normalizePath(path);
const parts = normalized.split('.').filter(Boolean);
let cur = root;
for (const part of parts) {
if (cur == null) return undefined;
cur = cur[part];
}
return cur;
}
let val;
try { val = getByPath(window, pathExpr); } catch(e) { return JSON.stringify({ exists: false, error: e.message, skippedPaths: [] }); }
if (val === undefined) return JSON.stringify({ exists: false, skippedPaths: [] });
return JSON.stringify({ exists: true, value: safe(val, pathExpr, 0), skippedPaths: skipped });
})
`;
for (const varName of varNames) {
try {
const r = await session.send('Runtime.evaluate', {
expression: `(${safeSerializerSrc})(${JSON.stringify(varName)})`,
returnByValue: true,
});
variables[varName] = r?.result?.value
? JSON.parse(r.result.value)
: { exists: false, error: r?.result?.description ?? 'unknown' };
} catch (e) {
variables[varName] = { exists: false, error: String(e.message) };
}
process.stderr.write(
` ${varName}: ${variables[varName].exists ? 'found' : 'not found'}${variables[varName].skippedPaths?.length ? ` (${variables[varName].skippedPaths.length} paths skipped)` : ''}\n`,
);
}
timings.varsMs = Date.now() - varsStart;
}
// ── close or keep tab ─────────────────────────────────────────────────────────
// Default behaviour:
// new tab (no --tab-id) → close unless --keep-tab
// existing tab (--tab-id) → keep unless --close
const shouldClose = closeTab || (!keepTab && !tabId);
session.close();
if (shouldClose) await fetch(`${CDP_BASE}/json/close/${tab.id}`);
// ── output ────────────────────────────────────────────────────────────────────
const elapsedMs = Date.now() - tStart;
timings.totalMs = elapsedMs;
const result = {
...(keepTab || tabId ? { tabId: tab.id } : {}),
activeTabId: tab.id,
url: targetUrl ?? tab.url,
capturedAt: stamp(),
elapsedMs,
waitUntil: effectiveWaitUntil,
actionWait: effectiveActionWait,
requestedWaitUntil: waitUntil,
requestedActionWait: actionWait,
earlyStop: lastWait.earlyStop,
timedOut: lastWait.timedOut,
timings,
total: logs.length,
errors: logs.filter((l) => l.level === 'error').length,
warns: logs.filter((l) => l.level === 'warn').length,
...(tabTransition ? { tabTransition } : {}),
...(clickTarget ? { click: clickResult } : {}),
...(fillArg ? { fill: fillResult } : {}),
...(selectArg ? { select: selectResult } : {}),
...(dumpDom ? { dom: domSnapshot } : {}),
...(evalExpr ? { evalResult } : {}),
...(varNames.length ? { variables } : {}),
...(noEntries
? {}
: {
entries: entriesLimit
? logs.slice(Math.max(0, logs.length - entriesLimit))
: logs,
}),
};
process.stderr.write(
`Done: ${result.errors} errors, ${result.warns} warns, ${result.total} total\n`,
);
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
#!/usr/bin/env node
function parseArgs(argv) {
const args = {};
for (let i = 2; i < argv.length; i++) {
const arg = argv[i];
if (arg.startsWith('--')) {
const eqIdx = arg.indexOf('=');
if (eqIdx >= 0) {
args[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1);
} else {
args[arg.slice(2)] = argv[i + 1] || '';
i++;
}
}
}
return args;
}
function main(ctx) {
const results = [];
const bundlerName = (ctx.bundler && ctx.bundler.name) || 'unknown';
const hasTypescript = Boolean(
ctx.dependencies && ctx.dependencies.typescript,
);
results.push({
code: 'PERF',
severity: 'info',
message:
'Enable dev.disableAssetsAnalyze during local development to reduce bundle analysis overhead and speed up HMR and builds.',
context: { bundler: bundlerName },
});
if (bundlerName === 'rspack' || bundlerName === 'rsbuild') {
results.push({
code: 'PERF',
severity: 'info',
message:
'Rspack/Rsbuild project detected. Setting splitChunks.chunks to "async" is recommended to reduce initial bundle size.',
context: { bundler: bundlerName },
});
}
if (hasTypescript) {
results.push({
code: 'PERF',
severity: 'info',
message:
'TypeScript dependency detected. If type generation (DTS) overhead is too high, consider disabling DTS or using ts-go to optimize the type-checking pipeline.',
context: { typescript: ctx.dependencies.typescript },
});
}
process.stdout.write(
`${JSON.stringify({ context: ctx, results }, null, 2)}\n`,
);
}
const args = parseArgs(process.argv);
main(JSON.parse(args.context));