npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bun-plugin-qiankun

v1.0.1

Published

Transform Bun-built HTML for qiankun micro-frontend compatibility. Inspired by vite-plugin-qiankun.

Readme

bun-plugin-qiankun

Transform Bun-built HTML for qiankun micro-frontend compatibility. Inspired by vite-plugin-qiankun.

Why?

When qiankun loads a sub-app, it fetches the sub-app's index.html and executes its scripts inside a JS sandbox (ProxySandbox). There's a critical difference between how scripts are executed:

  • Inline scripts (eval'd via new Function) — see the sandbox proxy as window
  • ES modules (loaded via import()) — see the real window, not the proxy

This means:

  1. Static <script type="module"> paths resolve relative to the main app's origin, not the sub-app's — causing 404s
  2. window.__POWERED_BY_QIANKUN__ is set on the proxy, so ESM modules cannot read it

This plugin solves both problems by transforming the HTML at build time.

What it does

The transformQiankunHtml() function performs these transformations on your built index.html:

  1. Entry script → dynamic import() — Converts <script type="module" src="./assets/index.js"> into import('/childapps/sub-app/assets/index.js') with the correct URL prefix, so the module loads from the sub-app's path

  2. Lifecycle bridge injection — Injects a <script> that creates window['appName'] with Promise-based bootstrap/mount/unmount/update methods. These use Symbol.for() for deferred binding — qiankun can call them immediately, and they resolve once the ESM module finishes loading

  3. Lifecycle resolution — After the dynamic import() resolves, connects the sub-app's actual lifecycle functions (registered via exportQiankunLifeCycles) to the Promise bridge

  4. Modulepreload transformation — Converts static <link rel="modulepreload"> tags to dynamic creation with the correct URL prefix

Installation

bun add bun-plugin-qiankun

Examples

The examples/ directory contains complete working examples:

| Directory | Description | |-----------|-------------| | app1 | Vite + React sub-app | | app1-bun | Bun + React sub-app | | app2 | Vite + Vue sub-app | | app2-bun | Bun + Vue sub-app | | mainvue | Vue 3 host app | | mainreact | React host app | | mainvanilla | Vanilla JS host app (no framework) |

Each sub-app can be debugged standalone (bun run start) or with a host app (bun run dev).

Usage

1. Build script (build.ts)

Use HTML as the entrypoint — Bun automatically transpiles TSX/TS, extracts CSS, and updates asset references in the HTML:

import { transformQiankunHtml } from 'bun-plugin-qiankun';
import { rm } from 'fs/promises';
import { join } from 'path';

const APP_NAME = 'my-sub-app';
const PUBLIC_PATH = '/childapps/my-sub-app/';

const distDir = join(import.meta.dir, 'dist');
await rm(distDir, { recursive: true, force: true });

const result = await Bun.build({
  entrypoints: ['./index.html'],
  outdir: distDir,
  format: 'esm',
  target: 'browser',
  minify: true,
  publicPath: PUBLIC_PATH,
  naming: {
    entry: 'assets/[name].[ext]',
    chunk: 'assets/[name].[ext]',
    asset: 'assets/[name].[ext]',
  },
});

if (!result.success) {
  console.error(result.logs);
  process.exit(1);
}

// Move index.html from assets/ to dist root
await Bun.write(
  join(distDir, 'index.html'),
  await Bun.file(join(distDir, 'assets/index.html')).text()
);
await rm(join(distDir, 'assets/index.html'));

// Apply qiankun HTML transformation
const htmlPath = join(distDir, 'index.html');
const rawHtml = await Bun.file(htmlPath).text();
const transformedHtml = transformQiankunHtml(rawHtml, {
  appName: APP_NAME,
  changeScriptOrigin: false,
  publicPath: PUBLIC_PATH,
});
await Bun.write(htmlPath, transformedHtml);

2. HTML template (index.html)

Reference your source files directly — Bun handles transpilation:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>my-sub-app</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./src/main.tsx"></script>
  </body>
</html>

After bun build, the output dist/index.html will reference the bundled JS/CSS automatically.

3. Dev server (dev.ts)

For development with hot rebuild, use Bun.build() in-memory bundling:

import { serve } from 'bun';
import { transformQiankunHtml } from 'bun-plugin-qiankun';
import { watch } from 'fs';

const APP_NAME = 'my-sub-app';
const BASE_PATH = '/childapps/my-sub-app';
const PORT = 3001;

const rootDir = import.meta.dir;
let jsCode = '';
let html = '';

async function bundle() {
  const result = await Bun.build({
    entrypoints: ['./src/main.tsx'],
    format: 'esm',
    target: 'browser',
    minify: false,
    naming: '[name].[ext]',
  });
  if (!result.success) {
    for (const msg of result.logs) console.error(msg);
    return;
  }

  let cssCode = '';
  for (const o of result.outputs) {
    const text = await o.text();
    if (o.path.endsWith('.js')) jsCode = text;
    else if (o.path.endsWith('.css')) cssCode += text;
  }

  let h = await Bun.file(`${rootDir}/index.html`).text();
  h = h.replace(/src="\.\/src\/main\.tsx"/, 'src="./assets/main.js"');
  if (cssCode) h = h.replace('</head>', `<style>${cssCode}</style></head>`);
  html = transformQiankunHtml(h, {
    appName: APP_NAME,
    changeScriptOrigin: false,
    publicPath: `http://localhost:${PORT}${BASE_PATH}/`,
  });
}

await bundle();

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, OPTIONS',
  'Access-Control-Allow-Headers': '*',
};

serve({
  port: PORT,
  routes: {
    [`${BASE_PATH}/assets/main.js`]: () =>
      new Response(jsCode, { headers: { 'Content-Type': 'application/javascript', ...corsHeaders } }),
    [`${BASE_PATH}/*`]: async (req) => {
      const url = new URL(req.url);
      const relative = url.pathname.slice(BASE_PATH.length + 1);
      const file = Bun.file(`${rootDir}/${relative}`);
      if (await file.exists()) return new Response(file, { headers: corsHeaders });
      return new Response(html, { headers: { 'Content-Type': 'text/html', ...corsHeaders } });
    },
    [BASE_PATH]: () => new Response(null, { status: 302, headers: { Location: `${BASE_PATH}/` } }),
  },
});

watch(`${rootDir}/src`, { recursive: true }, async () => {
  await bundle();
});

4. Entry file (src/main.tsx for React)

import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { exportQiankunLifeCycles } from 'bun-plugin-qiankun/client';
import App from './App';

let root: any = null;

function isInQiankun() {
  return window.location.pathname.startsWith('/my-sub-app');
}

function render(props: any = {}) {
  const { container } = props;
  const el = container ? container.querySelector('#root') : document.getElementById('root');
  root = createRoot(el!);
  root.render(
    <BrowserRouter basename={isInQiankun() ? '/my-sub-app' : '/childapps/my-sub-app'}>
      <App />
    </BrowserRouter>
  );
}

exportQiankunLifeCycles({
  name: 'my-sub-app',
  bootstrap() { console.log('bootstrap'); },
  mount(props) { render(props); },
  unmount() { root?.unmount(); root = null; },
  update(props) { console.log('update', props); },
});

if (!isInQiankun()) {
  render();
}

5. Entry file (src/main.ts for Vue)

import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import { exportQiankunLifeCycles } from 'bun-plugin-qiankun/client';
import App from './App';

let app: any = null;

function isInQiankun() {
  return window.location.pathname.startsWith('/my-sub-app');
}

function render(props: any = {}) {
  const { container } = props;
  const el = container ? container.querySelector('#app') : document.getElementById('app');
  const router = createRouter({
    history: createWebHistory(isInQiankun() ? '/my-sub-app' : '/childapps/my-sub-app'),
    routes: [
      { path: '/', component: App },
    ],
  });
  app = createApp(App);
  app.use(router);
  app.mount(el!);
}

exportQiankunLifeCycles({
  name: 'my-sub-app',
  bootstrap() { console.log('bootstrap'); },
  mount(props) { render(props); },
  unmount() { app?.unmount(); app = null; },
});

if (!isInQiankun()) {
  render();
}

6. Register in host app

import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'my-sub-app',
    entry: '/childapps/my-sub-app/',
    container: '#subapp-container',
    activeRule: '/my-sub-app',
  },
]);

start({ sandbox: { experimentalStyleIsolation: true } });

API

transformQiankunHtml(html, options)

Transforms an HTML string for qiankun compatibility.

| Option | Type | Default | Description | |--------|------|---------|-------------| | appName | string | required | Sub-app name, must match the name in exportQiankunLifeCycles and registerMicroApps | | changeScriptOrigin | boolean | true | When true, uses window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__ at runtime for cross-origin loading. When false, uses the static publicPath | | publicPath | string | '' | Absolute public path (e.g. /childapps/my-app/). Used when changeScriptOrigin is false to convert relative asset paths to absolute |

exportQiankunLifeCycles(lifecycle)

Registers lifecycle hooks on window for qiankun to call.

interface QiankunLifeCycle {
  name: string;
  bootstrap?: (props?) => void | Promise<void>;
  mount?: (props?) => void | Promise<void>;
  unmount?: (props?) => void | Promise<void>;
  update?: (props?) => void | Promise<void>;
}

qiankunWindow

A reference to window.proxy || window. Useful for accessing qiankun's sandbox proxy in inline script contexts. Note: inside ESM modules loaded via import(), this still resolves to the real window (not the proxy).

FAQ

Why can't I use window.__POWERED_BY_QIANKUN__ in my ESM module?

In qiankun's ProxySandbox, __POWERED_BY_QIANKUN__ is set on the proxy window, not the real window. Inline scripts (executed via eval/new Function) see the proxy as their window. But ESM modules loaded via import() see the real window — so window.__POWERED_BY_QIANKUN__ is undefined inside your bundled code.

Solution: Use URL-based detection instead:

function isInQiankun() {
  return window.location.pathname.startsWith('/your-active-rule');
}

Why use publicPath instead of changeScriptOrigin?

  • changeScriptOrigin: true (default) — Asset URLs are prefixed at runtime with window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__. Use this for cross-origin loading (e.g., dev mode where sub-app runs on a different port).
  • changeScriptOrigin: false + publicPath — Asset URLs are converted to absolute paths at build time. Use this for same-origin deployment (e.g., nginx serving all apps on one domain). This is simpler and avoids runtime URL computation.

How does this compare to vite-plugin-qiankun?

| Feature | vite-plugin-qiankun | bun-plugin-qiankun | |---------|--------------------|--------------------| | Build tool | Vite | Bun | | HTML transformation | Vite transformIndexHtml hook | Post-build transformQiankunHtml() | | Dev server | Vite dev server | Bun serve() + Bun.build() in-memory bundling | | Lifecycle bridge | Same mechanism | Same mechanism | | Modulepreload transform | Yes | Yes |

Why a function instead of a Bun plugin?

Bun's plugin API (Bun.plugin()) provides onLoad/onResolve hooks for module resolution, but has no hook for post-build HTML transformation. Vite can implement vite-plugin-qiankun because Rollup/Vite exposes transformIndexHtml. Bun doesn't have an equivalent.

The function approach is simple, explicit, and works with any build setup:

const transformed = transformQiankunHtml(rawHtml, { appName, publicPath });

Vue with Bun — what about .vue SFC files?

Bun's plugin system cannot intercept .vue file resolution (Bun treats unknown extensions as static assets). Workarounds:

  1. Use .ts files with defineComponent({ template: '...' }) and the full Vue build (vue/dist/vue.esm-bundler.js) for runtime template compilation
  2. Pre-compile .vue files to .ts before running bun build

Clean

To clean all node_modules and dist in the root and all example sub-directories:

npx rimraf node_modules dist examples/*/node_modules examples/*/dist

License

MIT