qiankun

GitHub

๐Ÿ“ฆ ๐Ÿš€ Blazing fast, simple and complete solution for micro frontends.

RAW Rules

AGENTS.md

# QIANKUN PROJECT KNOWLEDGE BASE

**Updated:** 2026-07-06 ยท **Commit:** dcc42ae4 ยท **Branch:** next (qiankun 3.0, active dev)

Qiankun is a micro-frontend framework built on [single-spa](https://github.com/single-spa/single-spa). v3 rewrites the runtime around **streaming HTML-entry loading**, a **Proxy-membrane JS sandbox**, and native **ESM-sandbox** execution. It is a pnpm monorepo built with Vite 8; `tsc` emits package declarations.

> Requires Node `^22.15 || >=24`, `[email protected]` (see `packageManager`). Never use npm/yarn at the root.

## STRUCTURE

```
qiankun/
โ”œโ”€โ”€ packages/
โ”‚   โ”œโ”€โ”€ qiankun/         # Facade: public APIs (register/loadMicroApp, start, prefetch) + loadApp orchestration
โ”‚   โ”œโ”€โ”€ sandbox/         # JS isolation: Proxy membrane + Compartment  โ†’ packages/sandbox/AGENTS.md
โ”‚   โ”œโ”€โ”€ loader/          # Streaming HTML-entry loader (writable-dom)  โ†’ packages/loader/AGENTS.md
โ”‚   โ”œโ”€โ”€ shared/          # Transpilers, fetch-utils, module-resolver, ESM-sandbox engine โ†’ packages/shared/AGENTS.md
โ”‚   โ”œโ”€โ”€ single-spa/      # Vendored single-spa fork (@qiankunjs/single-spa, upstream 7.0 @ ce0f925a) โ†’ packages/single-spa/README.md
โ”‚   โ”œโ”€โ”€ ui-bindings/     # <MicroApp> components: react/ vue/ shared/ (dogfooded by examples/{main,vue-host})
โ”‚   โ””โ”€โ”€ bundler-plugin/  # Webpack(4/5) + Vite plugins: mark entry script, fix output library
โ”œโ”€โ”€ skills/              # Agent skills (installed by users via `npx skills add umijs/qiankun`) โ€” content, not code: no build/publish
โ”œโ”€โ”€ e2e/                 # Playwright, runs against BUILT dist โ†’ e2e/README.md
โ”œโ”€โ”€ examples/            # Runnable demos: two hosts (main = @qiankunjs/react, vue-host = @qiankunjs/vue) + react/vue/purehtml/webpack
โ””โ”€โ”€ docs/                # VitePress site (docs/rfcs holds design RFCs)
```

## PROGRESSIVE DISCLOSURE โ€” read the scoped doc before editing a package

| Working inโ€ฆ | Read first |
| --- | --- |
| `packages/sandbox/**` | [`packages/sandbox/AGENTS.md`](packages/sandbox/AGENTS.md) โ€” membrane, compartment, patchers, ESM globals |
| `packages/loader/**` | [`packages/loader/AGENTS.md`](packages/loader/AGENTS.md) โ€” streaming pipeline, head virtualization |
| `packages/shared/**` | [`packages/shared/AGENTS.md`](packages/shared/AGENTS.md) โ€” transpilers, fetch decorators, **esm-sandbox** |
| `e2e/**` | [`e2e/README.md`](e2e/README.md) โ€” Playwright layout, fixtures, anti-flake rules |

## ARCHITECTURE (big picture)

`loadApp` (`packages/qiankun/src/core/loadApp.ts`) is the orchestrator. Per micro-app it wires:

1. **fetch** โ€” decorated `window.fetch`: `makeFetchCacheable(makeFetchRetryable(makeFetchThrowable(fetch)))`.
2. **sandbox** โ€” `createSandbox()` builds a Proxy-membrane `window`/`document` view; patchers (dynamicAppend, timers, listeners, history) each return a `free()` cleanup called on unmount. Without a container it uses the JS-only preset; a container enables DOM containment.
3. **loader** โ€” `loadEntry(entry, container, opts)` streams the HTML entry through `writable-dom`, virtualizing `<head>` โ†’ `<qiankun-head>` and running each node through a `nodeTransformer`.
4. **transpilers** (`shared/assets-transpilers`) rewrite each script/link/style node before it hits live DOM.

Micro apps sharing one container element take FIFO turns for their DOM writes through the **container occupancy gate** (`core/containerOccupancy.ts`, two critical sections: load-phase streaming and mountโ†’unmount; see `docs/rfcs/container-occupancy-gate.md`).

Two execution paths, chosen per script type:

- **Classic** (`<script entry>`, UMD/global): source is wrapped and run via a **blob URL** scoped to the sandbox membrane. The app's export = `sandbox.latestSetProp` (the last global the entry script set).
- **ESM** (`<script type="module">`): handled by `EsmSandboxEngine` (`shared/esm-sandbox`). Modules are fetched, lexer-rewritten to route globals through the membrane, given synthetic specifiers via a dynamically injected **import map**, and evaluated in order. The engine also handles dynamic `import()`.

**Style isolation** (`shared/assets-transpilers/style.ts` + `link.ts`) uses CSS `@scope` at runtime; external stylesheets become blob-`<link>`s so `@scope` can wrap them. Opt-in via `styleIsolation`.

Internal dependency graph (never invert it):

```
qiankun โ†’ loader โ†’ sandbox โ†’ shared
                   sandbox โ†’ shared
qiankun โ†’ single-spa   (vendored fork; zero deps, bottom of the graph โ€” must not import shared/sandbox/loader)
ui-bindings/{react,vue} โ†’ ui-bindings/shared โ†’ qiankun
```

## COMMANDS

```bash
pnpm install                 # install all workspace deps

# build (Vite 8 โ†’ package JavaScript; tsc โ†’ declarations)
pnpm run build               # build everything (packages + examples)
pnpm run build:packages      # build only packages/** (prereq for e2e & examples)

# unit tests โ€” vitest + happy-dom, aliased to src (NO build needed, see vitest.config.ts)
pnpm run test                            # all packages
pnpm --filter @qiankunjs/shared run test # one package
pnpm --filter @qiankunjs/sandbox exec vitest run path/to.test.ts   # single file
pnpm --filter @qiankunjs/shared  exec vitest run -t "test name"    # single test by name

# e2e โ€” Playwright against BUILT dist (see e2e/README.md)
pnpm run test:e2e            # build all + run chromium suite

# lint / format / full CI gate
pnpm run eslint              # eslint packages/
pnpm run prettier:check      # prettier -c .
pnpm run ci                  # build + eslint + prettier:check (what CI runs)

# dev
pnpm run start:example       # build packages + run all example apps in parallel
pnpm run docs:dev            # VitePress docs
```

## CONVENTIONS (enforced by ESLint flat config โ€” `pnpm run eslint` will reject violations)

TypeScript is strict + type-checked (`typescript-eslint`'s type-checked flat config):

- **No `any`** โ€” `no-explicit-any` auto-fixes to `unknown`. No `as any`, `@ts-ignore`, `@ts-expect-error`.
- **Inline type imports** โ€” `import { type Foo, bar }`, not `import type { Foo }` on its own line (`consistent-type-imports`/`consistent-type-exports` with `fixStyle: inline-type-imports`).
- `no-unnecessary-condition` is an error โ€” don't guard values the types prove are always truthy.
- Unused vars/args must be prefixed `_` (`argsIgnorePattern: ^_`).
- `array-simple`: `T[]` for simple, `Array<T>` for complex element types.
- JS-wide: `no-else-return` (no `else` after `return`), `object-shorthand`.
- Path alias `@qiankunjs/*` โ†’ `packages/*/src` (tsconfig + vitest); imports resolve to **source**, not dist.

Build/release:

- Vite 8 builds browser package JavaScript as ESM+CJS, while `tsc` emits declarations. Package entry fields and subpath exports must point at the corresponding `dist` outputs.
- TypeScript 7 is installed as `@typescript/native` and owns the `tsc` binary. The `typescript` dependency intentionally aliases `@typescript/typescript6` because TypeScript 7.0 has no programmatic API yet and tools such as `typescript-eslint` and `vue-tsc` still require that compatibility API.
- Versioning via **changesets**, but changesets are **auto-derived from Conventional Commits** in CI (`scripts/generate-changesets.mjs`) โ€” do **not** hand-write `.changeset/*.md`. Just land a well-formed conventional commit (`feat`/`fix`/`feat!`โ€ฆ); the release job maps changed files โ†’ packages and infers the bump. Each sub-package keeps its own `CHANGELOG.md` (changeset default, visible on npm); on publish `scripts/generate-release-notes.mjs` aggregates them into **one GitHub Release**, which can be polished after the fact via the `/release-changelog` skill (`gh release edit`). Full flow: `.changeset/README.md`.
- Conventional commits enforced by commitlint (`feat:`, `fix:`, `feat(esm-sandbox):`, โ€ฆ).

## DOCS & README CONVENTIONS

- **Chinese-first.** The primary audience is Chinese. `docs/zh-CN/` is the source of truth: write it to a native-author standard โ€” no translationese, no AI-flavored filler (ใ€Œ่ฎฉๆˆ‘ไปฌใ€ใ€Œๅ€ผๅพ—ๆณจๆ„็š„ๆ˜ฏใ€ใ€Œๅผบๅคง็š„/ๆ— ็ผ็š„ใ€), no calqued English syntax. The English pages align to the Chinese semantics, not the other way around; pure zh wording fixes don't require touching en. Same contract for `README.zh-CN.md` โ†” `README.md`: keep both editions in sync whenever either changes.
- **Mainland reachability is a hard constraint.** Google Fonts only via the `.cn` mirrors (`fonts.googleapis.cn`/`fonts.gstatic.cn`); never reference resources on domains unreachable from mainland China.
- **Domains**: canonical docs domain is `https://www.qiankunjs.com` (zh under `/zh-CN/`); `qiankun.umijs.org` stays alive and 307s to it; v2 docs live at `v2.qiankun.umijs.org`; live examples at `examples.qiankunjs.com`. Old-site URLs are kept working via `docs/public/_redirects` โ€” extend it when moving pages, never break inbound links.
- **Consistency rules**: terminology never drifts (ไธปๅบ”็”จ/ๅพฎๅบ”็”จ/ๆฒ™็ฎฑ/้š”็ฆป่†œ; qiankun always lowercase); link text in reference lists ("็›ธๅ…ณๅ†…ๅฎน"/"็ปง็ปญ้˜…่ฏป"/"ๅปถไผธ้˜…่ฏป") equals the target page's H1; API pages use ใ€Œๅ‡ฝๆ•ฐ็ญพๅใ€/ใ€Œ้ป˜่ฎคๅ€ผไธบ Xใ€; `guide/browser-support` is the single source of truth for browser requirements โ€” link it, don't restate version numbers elsewhere.
- Until 3.0 reaches npm `latest`, install commands must say `qiankun@rc`, and the site shows a version banner (layout-top slot in `docs/.vitepress/theme/index.js`; remove it together with `--vp-layout-top-height` once stable ships).
- **README GitHub alerts**: put the `[!WARNING]`/`[!NOTE]` marker on its own line followed by a blank `>` line. Prettier's `proseWrap: never` re-joins plain soft breaks, which silently breaks alert rendering.
- VitePress compiles every `.md` as a Vue SFC: bare `<Tag>` in prose/link text or `{{` in inline code breaks the build (fenced code blocks are safe).

## ANTI-PATTERNS (this project)

- **NEVER** put more than one `entry` script in an HTML entry โ€” the loader throws `QiankunError`.
- **ALWAYS** unmount micro-apps; `loadMicroApp`/patchers return handles/`free()` โ€” leaks break remount & multi-instance.
- In sandbox code, **never touch the real `window`/`document.head`** โ€” go through the proxied view.
- The vendored `loader/src/writable-dom/` fork accepts **generic designs only** โ€” never qiankun-coupled semantics; caller bookkeeping goes through its `assetTransformer` callback (see `packages/loader/AGENTS.md`).
- Don't invert the package dependency graph above (e.g. `shared` must not import `sandbox`).
- e2e: never `waitForTimeout`; use web-first assertions; all ports come from `e2e/ports.ts`.
- Performance work is judged in this order: **proportional impact first** (does the win scale with asset size / network / app scale โ€” e.g. eliminating a duplicate download), **readability second** (never trade it away for small wins), **constant absolute savings last** (fixed single-digit-ms pipeline costs are noise in real apps and rarely worth landing). Beware: the ~50ms benchmark fixture makes fixed milliseconds masquerade as percentages โ€” convert to absolute ms against realistic load times before deciding. And fix root causes at the source, not with per-call-site markers/exemptions downstream.

## NOTES

- Firefox doesn't support dynamically injected import maps โ†’ ESM-sandbox e2e tests are annotated `test.fail(firefox, โ€ฆ)` (expected failure, not skip). See `e2e/README.md`.
- Design decisions live in `docs/rfcs/` (e.g. the ESM-sandbox RFC).
- v3 roadmap: github.com/umijs/qiankun/discussions/1378.