Skip to content

feat(examples): add micro-frontend example with Hub + multi-framework sub-apps#296

Open
lzxb wants to merge 128 commits into
masterfrom
feat-micro-app-example
Open

feat(examples): add micro-frontend example with Hub + multi-framework sub-apps#296
lzxb wants to merge 128 commits into
masterfrom
feat-micro-app-example

Conversation

@lzxb
Copy link
Copy Markdown
Contributor

@lzxb lzxb commented May 7, 2026

Summary

BREAKING CHANGE: Redesign the router mounting API (rootappId) and add
first-class SSR hydration support. Replace all legacy examples with a unified
micro-frontend architecture demo.

Core API Changes

root replaced by appId

  • RouterOptions.root (string | HTMLElement) → appId (string)
  • Client: resolves via document.getElementById(appId)
  • Server: wraps renderToString() output in <div id="${appId}">
  • Default: 'app'

SSR hydration support

  • RouterMicroAppOptions adds hydration(el) callback
  • Server renders <div id="${appId}" data-ssr>...</div>
  • Client detects data-ssr, calls hydration() instead of mount(), then removes the attribute
  • Prevents DOM flicker and preserves interactive state

resolveLink hook

  • New RouterOptions.resolveLink option for custom router-link transformation

Dev-only SSR validation

  • renderToString() output is validated to contain exactly one root element
  • Catches framework-level SSR mismatches early

Example Projects

Removed 15+ legacy demos (router-demo-*, ssr-demo-*, ssr-vue2-host/remote).

Added examples/micro-app/ micro-frontend suite:

Package Framework
ssr-micro-hub Hub dispatcher with shared sidebar layout
ssr-micro-html Vanilla HTML + TypeScript
ssr-micro-vue2 Vue 2.7 + Composition API
ssr-micro-vue3 Vue 3.5 + SSR
ssr-micro-react React 18 + Hooks
ssr-micro-shared Shared Layout, BaseApp, SSR styles

Notable Fixes

  • Vue2: fix v-html hydration mismatch caused by inline style whitespace normalization
  • Vue2: remove wrapper div from renderToString so data-server-rendered attaches correctly
  • All frameworks: unify mount/unmount DOM lifecycle to prevent residue nodes during app switching

… sub-apps

- Add ssr-micro-shared for sharing @esmx/router across all sub-apps
- Add ssr-micro-html with native HTML + TypeScript
- Add ssr-micro-vue2 with Vue 2.7 + Composition API
- Add ssr-micro-vue3 with Vue 3.5 + SSR
- Add ssr-micro-react with React 18 + Hooks
- Add ssr-micro-hub to aggregate all sub-app routes
- Configure modules.exports for framework library sharing (vue, react, react-dom)
- Configure server-only exports for vue-server-renderer and @vue/server-renderer
- Add README.md and README.en.md for each package
- Remove outdated ssr-demo-html and ssr-demo-preact-htm examples
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 7, 2026

Deploying esmx with  Cloudflare Pages  Cloudflare Pages

Latest commit: d77acd2
Status: ✅  Deploy successful!
Preview URL: https://58f9b270.esmx.pages.dev
Branch Preview URL: https://feat-micro-app-example.esmx.pages.dev

View logs

@lzxb lzxb force-pushed the feat-micro-app-example branch from 37060e3 to 0070dfb Compare May 7, 2026 12:44
Dev added 24 commits May 7, 2026 20:53
… cards

- Add consistent styling system with CSS animations
- Redesign home page with modern cards, icons, and feature section
- Update all sub-apps with consistent card-based layout
- Add fade-in animations for page transitions
- Use container element pattern for clean unmounting
- Improve visual hierarchy and responsive design
…igation

- Create useLayout() composable in ssr-micro-shared
- Add consistent sidebar with navigation across all micro-apps
- Integrate layout into Vue2, Vue3, React, HTML, and Hub home page
- Remove global layout styles from entry.server.ts
- Use fixed DOM IDs (esmx-sidebar, esmx-layout-footer) across all apps
- Only update active state and rebind events when DOM already exists
- Avoid DOM recreation during micro-app transitions
- Layout is now a class instance with mount/unmount methods
- useLayout() acts as a composable factory for Vue2/Vue3 setup usage
- Maintains shared DOM behavior to prevent flicker
…removing global animations

- Remove global fadeIn animation from entry.server.ts
- Return empty string in Layout.header if sidebar DOM already exists
- Prevent v-html from replacing existing sidebar on each app switch
- Vue3: Move app creation to factory function, use v-once in template
- React: Simplify app structure, add biome-ignore for safe HTML
- HTML: Move Layout creation to mount function
- Hub: Simplify home.ts Layout usage
…HomeApp)

- HtmlApp: class-based HTML micro-app with render/mount/unmount methods
- HomeApp: class-based Hub home page with render/mount/unmount methods
- Both follow Vue pattern: instance created in factory function
- Move SSR-specific deps (ssr-micro-shared, @esmx/router) to devDependencies
- Fix React app: useMemo for Layout, proper hook dependencies
- Fix Vue2 entry comments to match other sub-apps
- Remove unused private member in HtmlApp
- Add biome override for dangerouslySetInnerHTML in micro-app
Move vue-server-renderer and @vue/server-renderer imports into
renderToString() to prevent client-side module resolution errors.
These modules are server-only (client: false) and should not be
statically imported in client bundles.
…hrefs

router.resolveLink() generates absolute URLs based on the router's base URL.
During static build (postBuild), the base URL is hardcoded to localhost,
which causes all sidebar links to point to http://localhost:3000/... in
production deployments. This breaks SPA navigation and causes unexpected
jumps to localhost.

Fix: Use relative paths (e.g., /html) directly for href attributes.
The actual SPA navigation is handled by event delegation calling
router.push(), which correctly resolves paths against the current base.
Add RouterOptions.resolveLink option to allow transforming the result
of resolveLink() before it's returned. This enables use cases like
removing the domain from href attributes for static site generation.

Usage:
const router = new Router({
    base: new URL(base),
    resolveLink(link) {
        link.attributes.href = link.route.url.href
            .slice(link.route.url.origin.length) || '/';
        return link;
    }
});

Also update micro-app example to use resolveLink option instead of
hardcoding relative paths in hrefs.
Cloudflare Pages provides CF_PAGES_URL env var during build.
Use it to generate correct base URL with /ssr-micro-hub/ path prefix
so that resolveLink generates correct relative hrefs like
/ssr-micro-hub/html instead of /html.
@lzxb lzxb force-pushed the feat-micro-app-example branch from 94bc7e4 to d8f5f50 Compare May 8, 2026 08:27
Dev and others added 14 commits May 11, 2026 09:49
…lization

- Replace inline array creation in createLayer afterEach with a
  constant Set to avoid allocating on every navigation event
- Add detailed comment explaining the self-assignment of hash in
  isUrlEqual for trailing empty hash normalization

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a dark-themed landing page for the Esmx framework featuring:
- Hero section with ESM architecture visualization
- Pain points comparison (traditional vs Esmx)
- Six core feature cards
- Terminal-style quick start demo
- Framework ecosystem showcase (Vue, React, Preact, Solid, HTML5, Svelte)
- Mobile-responsive design with scoped CSS

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rspress 2.0.0-rc.1 had an internal inconsistency where @rspress/core
resolved to react@19.2.4 while @rspress/runtime resolved to react@19.2.6.
This caused the bundler to include two react-router instances in the SSG
bundle, breaking React Context and causing useLocation invariant errors.

Upgrading to @rspress/core@2.0.10 (matching @rspress/plugin-llms) ensures
all Rspress sub-packages use the same React version.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Keep only ssr-micro-react and ssr-micro-vue3 as examples,
remove other micro-apps from links and postBuild pages.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…icro-app examples

- Remove ssr-micro-shared/src/unhead-client.ts and unhead-server.ts
- Update all micro-apps to import unhead/client and unhead/server directly
- Remove unhead imports mapping from entry.node.ts files
- Fix Hub entry.node.ts to include all micro-app links
- Remove unused global head setup from Hub entry.client.ts
…rameworks

Replace generic app-a/b/c labels with actual framework names:
- vue, react, preact, html, solid, shared, utils

This better demonstrates Esmx's multi-framework micro-frontend capabilities.
- Auto-create head instance in BaseApp constructor
- Export renderSSRHead and types from ssr-micro-shared
- Remove unhead dependency from individual micro-apps
- Simplify all micro-apps by removing createHead/setRouterHead boilerplate
- Remove all @ts-expect-error comments for unhead imports
- Create ssr-micro-solid with SolidJS 1.9.3
- Configure Rspack with babel-loader and babel-preset-solid
- Implement SSR/hydration lifecycle for SolidJS
- Register in Hub with route /solid/
- Add SolidJS to shared layout navigation
- Update landing page animation to include SolidJS
Expand postBuild to pre-render all sub-pages (demo, html, vue2, vue3,
react, preact, preact-htm, solid) instead of only home, react and vue3.
This ensures all sub-pages have correct HTML with valid JS references.
Remove the unnecessary <div> wrapper from all micro-app renderToString
methods. The Router already wraps output in <div id=app data-ssr>,
so the extra <div> caused hydration DOM mismatch errors.
Vue2 was creating its own head via unhead/client despite BaseApp already
providing one. Since Vue2 uses the same generic unhead/client as BaseApp
(unlike Vue3/React which need framework-specific @unhead/xxx packages),
it can simply reuse this.head from the base class.

Changes:
- Remove private head = createHead() field
- Remove setRouterHead() call (already done in BaseApp constructor)
- Remove unused unhead/client import
@lzxb lzxb force-pushed the feat-micro-app-example branch 2 times, most recently from 05d5015 to 3481302 Compare May 13, 2026 03:42
- Compute SHA-384 integrity hashes during Rspack production builds
- Store integrity in manifest.json to avoid extra files
- Inject integrity into inline importmap for browser validation
- Skip integrity computation in development to preserve HMR performance
- Fixes intermittent module resolution errors caused by stale CDN cache
@lzxb lzxb force-pushed the feat-micro-app-example branch from 3481302 to fb182cb Compare May 13, 2026 04:05
Dev added 12 commits May 13, 2026 12:45
Remove unnecessary <div> wrapper from SolidJS renderToString.
The Router already wraps output in <div id=app data-ssr>,
so the extra <div> caused hydration DOM mismatch errors:
'Cannot read properties of undefined (reading done)'
at routes.*.final.mjs - Solid expects container to match SSR root
that owns _$HY state init markers.
Scoped to micro-app example only, not the core framework.

solid-js hydrate() requires globalThis._$HY to exist. Without
generateHydrationScript() in the SSR output this object is
undefined, causing 'Cannot read properties of undefined (reading
"done")' on /solid/ page load.

Propagate hydration script injection via app.renderToString() return value.
MicroApp._update() uses root.firstElementChild to find the SSR
content container. Placing <script> before the wrapper div caused
hydrate() to receive the script element instead of the SSR markup.

Move the hydration script inside the wrapper <div> so it remains
the first child element, preserving the correct hydration target.
SolidJS app created Layout instance but never called mount() to
attach sidebar click handlers. Without JS interception, <a href>
causes full page navigation instead of SPA routing.

Add onMount/onCleanup lifecycle hooks consistent with React and
Preact micro-apps.
Add ssr-micro-lit example using Lit's html template system for server-side
rendering (renderThunked + collectResult) and @lit-labs/ssr-client hydrate()
for client-side hydration.

- Zero custom Rspack loader needed (Lit is standard TypeScript)
- Reuses BaseApp, Layout, and head-manager from ssr-micro-shared
- Registers /lit/ route in hub with sidebar nav entry and Lit logo SVG
Lit SSR renders complete HTML via renderThunked() with unsafeHTML()
for header/footer content. The unsafeHTML() output lacks Lit's
hydration marker comments that hydrate() expects, causing
'hrefation value mismatch' error on page load.

Skip client-side Lit hydration entirely — SSR output is already
complete, only layout.mount() is needed for sidebar events.

Remove unused @lit-labs/ssr-client dependency.
renderThunked() produces <!--lit-part--> hydration markers in SSR
output that trigger 'Hydration value mismatch' errors in Lit's
client runtime even when hydrate() is not explicitly called.

Drop @lit-labs/ssr dependency entirely. SSR now builds HTML strings
directly (matching HTML app pattern). Lit template system reserved
for CSR onMount() path only.

Bundle: 206KB → 20KB
Remove unsafeHTML() from Lit template — it caused hydration mismatch
because SSR output lacks markers that hydrate() expects for dynamic
content expressions.

Instead, use empty placeholder containers in the template. SSR fills
them via string replacement (safe because hydrate() ignores extra
content in static elements). Client fills them via DOM after
hydrate()/render().

Restore @lit-labs/ssr and @lit-labs/ssr-client dependencies.
Use official Lit SSR pattern: ssrHtml (server-only, no markers) wraps
hydratable lit html (with markers).

- Outer template (ssrHtml): header/footer via unsafeHTML — static,
  not hydrated. Rendered only on server.
- Inner template (lit html): content card with markers — hydrated
  on client via data-lit-content selector.
- onHydration: hydrate only the inner content, not header/footer.
- onMount: full CSR with inline header/footer HTML + render().

This avoids the fundamental mismatch between unsafeHTML output and
Lit's hydration marker expectations.
Svelte 5 uses compiler-first approach with runes ().
Pair svelte-loader with Rspack chain config (generate: client/server).

Rspack v2 doesn't handle node: URI scheme used by Svelte's
internal/server for optional AsyncLocalStorage. Use IgnorePlugin
to skip node:async_hooks — Svelte already has .then(noop, noop)
fallback for import failures.

Registered /svelte/ route with Svelte logo in sidebar nav.
pkg:svelte externalization caused temporal dead zone errors
('Cannot access c before initialization') because the external
svelte chunk and the compiled App.svelte (importing from
svelte/internal/client) created a circular initialization order.

Bundle svelte inline — the IgnorePlugin already handles the
node:async_hooks scheme issue that Rspack can't resolve.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant