vuejs/core
github.com/vuejs/core ↗ · 696 files · 5.5 MB
vuejs/core Getting Started Please follow the documentation at vuejs.org!
Overview
This is the vuejs/core monorepo at v3.5.43 — the source of the `vue` npm package and its ten sibling `@vue/*` packages, split cleanly into a compiler half (compiler-core → compiler-dom → compiler-sfc/compiler-ssr) and a runtime half (reactivity → runtime-core → runtime-dom/server-renderer), glued by `@vue/shared`. The distinctive engineering shape is compile-time configurability: every package is built by a single `rollup.config.js` driven by `scripts/build.js` into up to seven output formats, with `__DEV__`/`__BROWSER__`/`__FEATURE_*`/`__COMPAT__` flags statically replaced so that dev warnings, the Options API, Suspense, and the Vue-2 compat layer all tree-shake away. It is written for framework maintainers and ecosystem tool authors rather than app developers: the public API surface is small, but the internal surface (renderer internals object, `createRenderer` for custom platforms, compiler `nodeTransforms`/`directiveTransforms`) is explicitly designed as an extension substrate. Two private packages — `sfc-playground` and `template-explorer` — are live-deployed dev tools that consume the built artifacts of the very repo that builds them.
System map
A pnpm workspace (`packages/*` + `packages-private/*`) of eleven published-or-private packages with a strict compiler/runtime separation enforced by lint rules. There is no server, database, or persistent storage layer — the "storage" is the filesystem (dist bundles) plus in-memory LRU caches in the compiler. Builds go through `scripts/build.js` → `rollup.config.js`, which fans one package out into esm-bundler/esm-browser/cjs/global/runtime variants; types are stitched separately via `tsc -p tsconfig.build.json --noCheck && rollup -c rollup.dts.config.js`. Tests run under Vitest with five named projects (unit, unit-gc, unit-jsdom, e2e, e2e-browser). Deployment is npm publish from a tag-triggered GitHub Actions workflow, plus continuous pre-release publishing via pkg-pr-new on every push.
- The workspace is defined by `pnpm-workspace.yaml` covering `packages/*` and `packages-private/*`, with a shared dependency `catalog:` pinning @babel/parser, entities, estree-walker, magic-string, source-map-js and vite, plus a `minimumReleaseAge: 1440` supply-chain guard and `trustPolicy: no-downgrade`.
- The published package graph is compiler-sfc → {compiler-core, compiler-dom}, compiler-dom → compiler-core, runtime-dom → runtime-core → reactivity, and `vue` → {compiler-dom, runtime-dom}; the contributing guide documents this as a mermaid diagram and forbids compiler packages importing runtime code (shared logic must go to `@vue/shared`).
- `packages/vue/src/index.ts` is the full-build entrypoint: it imports `compile` from `@vue/compiler-dom`, wraps it in `compileToFunction` with a module-level `compileCache` keyed by `genCacheKey(template, options)`, and calls `registerRuntimeCompiler(compileToFunction)` so runtime-core can compile `template` options on the fly.
- `packages/vue/src/runtime.ts` is the runtime-only entrypoint built as `dist/vue.esm-bundler.js`; it re-exports `@vue/runtime-dom` and stubs `compile` with a dev-only warning that names the exact alias to configure per build target.
- `scripts/build.js` resolves each target's `buildOptions.formats` from its package.json, deletes `dist` unless a specific `--formats` was requested, then shells out to `rollup -c` passing COMMIT (from `git rev-parse --short=7 HEAD`), NODE_ENV, TARGET, FORMATS, PROD_ONLY and SOURCE_MAP through `--environment`.
- `rollup.config.js` maps seven `PackageFormat` values (esm-bundler, esm-browser, cjs, global, esm-bundler-runtime, esm-browser-runtime, global-runtime) to concrete `dist/<name>.<format>.js` paths and, under NODE_ENV=production, additionally emits `.prod.js` configs minified with `@swc/core`.
- Compile-time feature flags are injected by `resolveDefine()` in rollup.config.js — `__COMPAT__`, `__FEATURE_SUSPENSE__` (always true), `__FEATURE_OPTIONS_API__` and `__FEATURE_PROD_DEVTOOLS__` (left as runtime-overridable `__VUE_*` globals only in bundler-ESM builds) — which is why dead-code paths guarded by these flags vanish from browser/global bundles.
- Cross-package imports resolve via three parallel mechanisms that must be kept in sync: `compilerOptions.paths` in tsconfig.json, the `entries` map generated by `scripts/aliases.js` (shared by Vitest and Rollup), and pnpm workspace links.
- CI is layered: `.github/workflows/ci.yml` calls the reusable `test.yml` and then runs a `continuous-release` job publishing every push via `pnpx pkg-pr-new publish --compact --pnpm './packages/*'`; `release.yml` fires only on `v*` tags, requires the `Release` environment, and runs `pnpm release --publishOnly`.
- `test.yml` splits into four jobs — unit-test (ubuntu), unit-test-windows, e2e-test (installs puppeteer Chromium and `playwright install chromium`, then runs `node scripts/verify-treeshaking.js`), and lint-and-test-dts (eslint, prettier check, `tsc --noEmit`, and `pnpm run test-dts`).
- `netlify.toml` contains only `NODE_VERSION = "24"` and `NPM_FLAGS = "--version"` to suppress Netlify's own install step, implying the actual playground build/publish commands are configured in the Netlify UI rather than in-repo. (inferred)
- Release mechanics live in `scripts/release.js`: it parses `--preid/--dry/--tag/--skipBuild/--skipTests/--skipGit/--publishOnly/--registry`, and appends `--provenance` to the npm publish flags only when `process.env.CI` is set and no custom `--registry` was given.
Feature catalog
There are no routes or screens in the product sense — the user-facing surface is (a) the public JS API exported from `@vue/runtime-core`/`@vue/runtime-dom`, (b) the compiler APIs consumed by vite-plugin-vue/vue-loader, (c) SSR render functions, (d) the `nr <script>` developer command surface in package.json, and (e) two deployed browser tools. Built-in components (Teleport, Suspense, KeepAlive, BaseTransition, Transition, TransitionGroup) and built-in directives (v-model, v-show, v-on modifiers) are shipped as first-class features handled specially in both the compiler and the renderer. Custom elements and lazy-hydration strategies are the newer feature surfaces.
- `packages/runtime-core/src/index.ts` is the canonical public API list: reactivity re-exports, `computed`, the `watch` family, all `on*` lifecycle hooks, `provide`/`inject`/`hasInjectionContext`, `nextTick`, `defineComponent`, `defineAsyncComponent`, `useAttrs`/`useSlots`/`useModel`/`useTemplateRef`/`useId`, `h`, `createVNode`, and the four built-in components.
- `createApp` and `createSSRApp` are distinct DOM entrypoints: the first calls `ensureRenderer()`, the second `ensureHydrationRenderer()` which swaps in a hydration-enabled renderer and flips a module-level `enabledHydration` latch, so the hydration code path is only pulled into the bundle when SSR apps are used.
- The App instance exposes `use`, `mixin`, `component`, `directive`, `mount`, `unmount`, `onUnmount`, `provide` and `runWithContext`, with plugin dedup via an `installedPlugins` WeakSet and app-level teardown callbacks collected in `pluginCleanupFns`.
- Lazy hydration is a shipped feature with four strategy factories — `hydrateOnIdle`, `hydrateOnVisible`, `hydrateOnMediaQuery`, `hydrateOnInteraction` — exported from runtime-core and exercised by dedicated HTML fixtures in the e2e suite.
- Web-component support is a full subsystem: `defineCustomElement`/`defineSSRCustomElement` plus a `VueElement` class with shadow-DOM style injection (`_injectChildStyle`) and the `useHost`/`useShadowRoot` composables that warn when called outside a custom-element-defined component.
- `@vue/compiler-sfc` is the public SFC toolchain: `parse`, `compileTemplate`, `compileStyle`/`compileStyleAsync`, `compileScript`, `rewriteDefault`/`rewriteDefaultAST`, plus type-resolution internals `resolveTypeElements`, `inferRuntimeType`, `invalidateTypeCache` and `registerTS` explicitly exported for build-tool integration.
- `<script setup>` macros supported by compileScript are enumerated in a `MACROS` constant — defineProps, defineEmits, defineExpose, defineOptions, defineSlots, defineModel and withDefaults — each with its own module under `packages/compiler-sfc/src/script/`.
- Style compilation supports scoped CSS, CSS modules and preprocessors: `compileStyle` builds a postcss pipeline from `pluginTrim` and `pluginScoped`, adds `postcss-modules` when `modules: true`, and `style/preprocessors.ts` lazily `require`s sass/less/stylus through an injectable `preprocessCustomRequire`.
- SSR exposes five output shapes from `@vue/server-renderer`: `renderToString`, `renderToSimpleStream`, `renderToNodeStream`, `pipeToNodeWritable`, `renderToWebStream` and `pipeToWebWritable` (plus deprecated `renderToStream`), with `initDirectivesForSSR()` run as an import side effect.
- The developer command surface in package.json includes `nr dev`, `nr build`, `nr build-dts`, `nr check`, `nr test`/`test-unit`/`test-e2e`/`test-dts`/`test-coverage`, `nr size` (with size-global/size-esm/size-esm-runtime sub-targets), `nr bench`/`bench-compare`, `nr dev-sfc`, `nr dev-compiler` and `nr release`.
- The SFC Playground (`packages-private/sfc-playground`) wraps `@vue/repl` with a Monaco editor, toggles for production mode / SSR mode / auto-save, a TypeScript version pin (`DEFAULT_TYPESCRIPT_VERSION = '6.0.3'`), and enables experimental `propsDestructure: true` by default.
- The Template Explorer (`packages-private/template-explorer`) renders compiler output side-by-side in Monaco, can switch between `@vue/compiler-dom` and `@vue/compiler-ssr` via an `ssrMode` toggle, and maps generated code back to source using `SourceMapConsumer` from source-map-js.
Data flow
Two largely independent pipelines meet at the component instance. Compile-time: template/SFC source string → tokenizer/parser → AST → transform passes (which mutate the AST and register runtime helpers) → codegen → render-function source. Runtime: props/slots enter via vnodes, reactive state is owned by `Dep` objects linked to `Subscriber`s, changes call `trigger()` which notifies effects, effects are batched into the scheduler queue, and the flush drives `patch()` which writes to the host platform through the injected `RendererOptions`. Output boundaries are exactly three: DOM mutations via `nodeOps`/`patchProp`, an SSR string/stream buffer, and generated JS source text.
- `baseCompile` is the single compile funnel: it resolves `prefixIdentifiers`, calls `baseParse(source, resolvedOptions)` when given a string, composes user `nodeTransforms`/`directiveTransforms` after the base preset, runs `transform(ast, ...)`, and returns `generate(ast, resolvedOptions)`.
- The base transform preset is order-sensitive and documented as such — transformVBindShorthand, transformOnce, transformIf, transformMemo, transformFor, then (non-browser only) trackVForSlotScopes + transformExpression, then transformSlotOutlet, transformElement, trackSlotScopes, transformText — with directive transforms keyed `on`/`bind`/`model`.
- compiler-dom layers platform behaviour on top by prepending `ignoreSideEffectTags`, adding `transformStyle` (+ dev-only `transformTransition` and `validateHtmlNesting`), overriding the core `model`/`on` directive transforms, and setting `transformHoist: stringifyStatic` for non-browser builds.
- compiler-ssr reuses the same `baseParse`/`transform`/`generate` primitives but forces `ssr: true`, `prefixIdentifiers: true`, `cacheHandlers: false`, `hoistStatic: false`, stores the raw options in a `rawOptionsMap` WeakMap keyed by AST root so slot sub-transforms can recover them, and swaps in ssr* node transforms.
- SFC parsing is memoised: `parse.ts` computes a source key and consults `parseCache`, an LRU of max 500 entries created by `createCache()` — which downgrades to a plain `Map` in `__GLOBAL__`/`__ESM_BROWSER__` builds where lru-cache is unavailable.
- `compileScript` merges `<script>` and `<script setup>` through a `ScriptCompileContext`, hard-errors if the two blocks declare different `lang`, returns non-JS/TS blocks untouched, and routes the no-setup case to `processNormalScript(ctx, scopeId)`.
- Type-only props/emits resolution reaches outside the SFC: `resolveFS` in resolveType.ts builds a filesystem host from `ctx.options.fs` or falls back to `ts.sys`, rewriting `.vue.ts` paths back to `.vue`, and walks tsconfig references to resolve imported types across files.
- Reactive reads flow through `track(target, type, key)` in dep.ts which links the active `Subscriber` to a `Dep`; writes flow through `trigger()` which bumps the module-global `globalVersion` counter used to short-circuit stale computed recomputation.
- `ReactiveEffect.run()` swaps in `activeSub`/`shouldTrack`, runs `cleanupEffect` + `prepareDeps` before the fn and `cleanupDeps` after, and dev-warns 'Active effect was not restored correctly' if `activeSub` drifted — the double-buffered dep-linking scheme that replaces Vue 3.4's Set-based deps.
- Component update jobs are id-ordered: `queueJob` fast-paths an append when the job id is ≥ the tail's id and otherwise binary-inserts via `findInsertionIndex`, ensuring parents flush before children; `queueFlush` schedules `flushJobs` on a resolved promise microtask.
- `flushJobs` iterates the queue with `callWithErrorHandling`, clears QUEUED flags in a `finally` block even after a throw, then calls `flushPostFlushCbs` and recurses if either queue refilled during the flush.
- All DOM writes funnel through the `rendererOptions` object = `extend({ patchProp }, nodeOps)` passed to `createRenderer`; inside the renderer they are reachable only as the `o` field of the `internals` object shared with Teleport, Suspense and KeepAlive.
Behavior traces
Three flows carry most of the system's weight: browser app startup, a reactive state change producing a re-render, and SFC compilation as invoked by a bundler plugin. A fourth, hydration, forks off startup at `createSSRApp`. The call chains below were read in order through runtime-dom → apiCreateApp → renderer → component → scheduler.
- Startup step 1: `createApp(RootComponent)` in runtime-dom calls `ensureRenderer()` (memoising a single module-level renderer created from `rendererOptions`), then in dev injects `isNativeTag` and the `compilerOptions` getter warnings before wrapping `app.mount`.
- Startup step 2: the wrapped `app.mount` calls `normalizeContainer` (a `document.querySelector` for string selectors, with a dev warning for closed ShadowRoots), copies `container.innerHTML` into `component.template` when the component has neither render nor template, clears `container.textContent`, calls the inner mount, then sets `data-v-app` and removes `v-cloak`.
- Startup step 3: the inner `mount` in apiCreateApp creates the root vnode via `createVNode(rootComponent, rootProps)`, stamps `vnode.appContext = context`, installs a dev-only `context.reload` HMR hook that re-renders a cloned vnode with `el = null`, then calls `render(vnode, rootContainer, namespace)` and finally sets `rootContainer.__vue_app__ = app` and calls `devtoolsInitApp`.
- Startup step 4: the renderer's `render` dispatches to `patch(container._vnode || null, vnode, ...)`, records `container._vnode = vnode`, and then — guarded by an `isFlushing` latch — drains `flushPreFlushCbs` and `flushPostFlushCbs` synchronously.
- Startup step 5: `patch` switches on vnode type (Text/Comment/Static/Fragment) and otherwise on `shapeFlag`, routing ELEMENT → `processElement`, COMPONENT → `processComponent`, TELEPORT → `TeleportImpl.process(..., internals)` and (under `__FEATURE_SUSPENSE__`) SUSPENSE → `SuspenseImpl.process(..., internals)`.
- Startup step 6: `mountComponent` calls `createComponentInstance`, registers HMR when `instance.type.__hmrId` exists, injects `internals` into the ctx when the vnode is a KeepAlive, calls `setupComponent(instance, false, optimized)`, and either registers the instance with the parent Suspense (when `instance.asyncDep` is set, inserting a Comment placeholder) or calls `setupRenderEffect`.
- Startup step 7: `setupComponent` runs `initProps` then `initSlots`, and for stateful components `setupStatefulComponent` creates `instance.accessCache`, wraps `instance.ctx` in `new Proxy(instance.ctx, PublicInstanceProxyHandlers)`, pauses tracking, and invokes `setup(props, setupContext)` through `callWithErrorHandling` with `ErrorCodes.SETUP_FUNCTION`.
- Startup step 8: `finishComponentSetup` performs on-the-fly compilation only when `!isSSR && compile && !Component.render`, merging app-level `compilerOptions`, component-level `compilerOptions` and `delimiters` into `finalCompilerOptions`, then installs the `RuntimeCompiledPublicInstanceProxyHandlers` proxy for `with`-block render functions and finally runs `applyOptions(instance)` when `__FEATURE_OPTIONS_API__`.
- Startup step 9: `setupRenderEffect`'s `componentUpdateFn` on first run fires `bm` hooks, then either hydrates (when `el && hydrateNode`, optionally deferring through `type.__asyncHydrate` for lazy-hydrated async components) or calls `renderComponentRoot(instance)` and patches; on completion it queues `onVnodeMounted`/`m`/`a` hooks via `queuePostRenderEffect`, sets `instance.isMounted = true` and nulls `initialVNode`/`container`/`anchor` to avoid mount-only memory leaks (#2458).
- Update trace: a reactive write calls `Dep.trigger` → subscriber `notify()` → `batch(sub)` unless the effect is already RUNNING without ALLOW_RECURSE; the component's effect scheduler calls `queueJob`, `queueFlush` schedules `flushJobs` on the microtask queue, and `flushJobs` re-enters `componentUpdateFn` on the update branch which resolves `next`, runs `bu`, re-renders and patches, then queues `u`.
- Hydration trace: `createSSRApp` → `ensureHydrationRenderer()` → `createHydrationRenderer(rendererOptions)` → `baseCreateRenderer(options, createHydrationFunctions)`, whose returned `[hydrate, hydrateNode]` pair is wired to the internals; `app.mount(container, true, ...)` then routes to `hydrate(vnode, rootContainer)` instead of `render`, and any structural divergence calls `logMismatchError()`.
- Full-build template trace: `compileToFunction` short-circuits on `compileCache[key]`, resolves a `#id` selector to `el.innerHTML` (annotated `__UNSAFE__`), defaults `isCustomElement` to `tag => !!customElements.get(tag)`, calls `compile(template, opts)`, then evaluates the generated code with `new Function('Vue', code)(runtimeDom)` and tags the result `_rc = true`.
Testing signals
Vitest is the only test runner, with 196 spec files collocated in per-package `__tests__` directories and five projects configured in `vitest.config.ts` to isolate environment needs. Coverage is v8-based over `packages/*/src/**` with an explicit exclusion list that documents what is deliberately not unit-tested. Type correctness is tested separately by compiling `packages-private/dts-test` against generated `.d.ts` files, and treeshaking is asserted by a bespoke script. There is no mutation testing, no snapshot-free contract layer, and no coverage threshold gate in CI.
- Five Vitest projects are declared: `unit` (node env, excludes e2e and the DOM packages), `unit-gc` (forks pool with `--expose-gc` for two server-renderer specs), `unit-jsdom` (vue/vue-compat/runtime-dom), `e2e` (jsdom) and `e2e-browser` (real Chromium via `@vitest/browser-playwright`, only for Transition and TransitionGroup).
- Coverage explicitly excludes `packages/vue-compat/**`, `vue/src/dev.ts`, `vue/src/runtime.ts`, `runtime-core/src/profiling.ts`, `featureFlags.ts`, `customFormatter.ts`, and — with a comment saying they are covered by e2e instead — `hydrationStrategies.ts` and `runtime-dom/src/components/Transition*`.
- `scripts/setup-vitest.ts` installs three custom matchers — `toHaveBeenWarned`, `toHaveBeenWarnedLast`, `toHaveBeenWarnedTimes` — built on a mocked `console.warn`, which is the house idiom for asserting dev-only warning behaviour throughout the suite.
- Every published package carries a `__tests__` directory (compiler-core, compiler-dom, compiler-sfc, compiler-ssr, reactivity, runtime-core, runtime-dom, runtime-test, server-renderer, shared, vue, vue-compat), totalling 196 `.spec.ts` files.
- Compiler tests lean on snapshots (`__snapshots__` directories exist under compiler-core, compiler-dom and compiler-sfc), which means codegen changes surface as large snapshot diffs rather than assertion failures.
- The e2e suite drives real browser behaviour across 14 specs including `todomvc.spec.ts`, `commits.spec.ts`, `markdown.spec.ts`, `grid.spec.ts`, `svg.spec.ts`, `vModel.spec.ts`, `memory-leak.spec.ts`, `ssr-custom-element.spec.ts` and `trusted-types.spec.ts`, with shared drivers in `e2eUtils.ts`/`e2eBrowserUtils.ts`.
- `pnpm test-e2e` first builds the global dev bundle (`node scripts/build.js vue -f global -d`) and only then runs the e2e projects, so e2e results reflect the bundled artifact rather than the aliased source.
- Type-level testing is a separate pipeline: `test-dts` runs `build-dts` then type-checks `packages-private/dts-built-test` and `packages-private/dts-test`, whose ~20 `.test-d.ts(x)` files cover defineComponent, h, tsx, directives, inject, watch, scheduler, reactivity and custom elements.
- `scripts/verify-treeshaking.js` reads the built dev and prod bundles and asserts dead-code elimination; it runs in CI as a step of the e2e-test job, making it the guard for the `__DEV__`/`__FEATURE_*` flag scheme.
- Test files get relaxed lint rules — `no-console`, `no-restricted-globals` and `no-restricted-syntax` are off under `**/__tests__/**` — but `vitest/no-disabled-tests` and `vitest/no-focused-tests` are errors, so a stray `.only` or `.skip` fails the lint job.
- Benchmarks are first-class: `prebench` builds reactivity as esm-browser and `bench`/`bench-compare` run `vitest bench --project=unit` writing to and diffing against `temp/bench.json`.
- CI does not enforce a coverage threshold — `test-coverage` exists as a script but no workflow invokes it; the jobs run `test-unit`, `test-e2e`, lint, `check` and `test-dts` only.
Risk map
The security surface is narrow but sharp: three code paths build render functions with `new Function`, two paths assign `innerHTML`, and the SFC compiler reads arbitrary files off disk and `require`s user-supplied preprocessors. Structurally, the biggest blast radius is `renderer.ts` (2670 lines, the single implementation behind every platform) and the `__COMPAT__` layer, which threads conditionals through core runtime code that most contributors never exercise. CI hygiene is unusually strong (pinned action SHAs, `persist-credentials: false`, org-membership gating on the ecosystem-ci trigger), so the residual risks are code-level rather than pipeline-level.
- Runtime template compilation evaluates generated code with `new Function('Vue', code)(runtimeDom)` (or `new Function(code)()` in global builds) in both the full build and the compat build — the intended design, but it means any path that can influence a template string achieves arbitrary JS execution.
- Two in-DOM template paths are annotated `__UNSAFE__` by the authors and explicitly push trust onto the user: `compileToFunction` reading `el.innerHTML` for a `#selector` template, and `app.mount` copying `container.innerHTML` into `component.template`.
- Trusted Types are only best-effort: `nodeOps.ts` creates a `'vue'` policy when `window.trustedTypes` exists and otherwise `unsafeToTrustedHTML` degrades to the identity function, so v-html/innerHTML writes pass through unwrapped in browsers or configurations without TT.
- `stringifyStatic` evaluates constant expressions at compile time with `new Function(\`return (${exp.content})\`)()`, and `validateExpression` similarly constructs a `new Function` to syntax-check user expressions — both run inside the build process, so a malicious SFC executes code in the developer's toolchain.
- `compiler-sfc` performs unconstrained file IO outside the repo root: `resolveFS` reads any path the type resolver walks to (imports, tsconfig `references`, `globalTypeFiles`), bounded only by whatever `options.fs` the host plugin supplies.
- Style preprocessors are loaded by dynamic `require` with a caller-injectable `preprocessCustomRequire`, so the module actually loaded for `lang="scss"` depends on the host's resolution context rather than a pinned dependency.
- `packages/runtime-core/src/renderer.ts` is 2670 lines and holds mountComponent, setupRenderEffect, the full patch switch and `patchKeyedChildren` in one closure-scoped factory; every change there affects DOM, SSR hydration, runtime-test and any third-party custom renderer simultaneously.
- The `__COMPAT__` layer is woven through hot core paths — mountComponent's `compatMountInstance`, finishComponentSetup's `inline-template` prop lookup, setupRenderEffect's `hook:beforeMount`/`hook:mounted` emissions — yet `packages/vue-compat/**` is excluded from coverage collection, so these branches are the least-verified code in the runtime.
- Suspense + async-setup interaction is a recurring correctness hotspot: the head commit had to change an unmount guard from `hydratedEl && !instance.scope.active` to `!instance.scope.active`, and `effect.ts` carries a long comment about effects orphaned when a component is unmounted while awaiting inside `withAsyncContext`.
- The scheduler's recursion guard is dev-only: `checkRecursiveUpdates` is wired in via `const check = __DEV__ ? ... : NOOP`, so an infinite update loop that only manifests in production silently spins instead of warning.
- Module-level mutable singletons create ordering hazards across tests and micro-frontends: `renderer`/`enabledHydration` in runtime-dom, `compile`/`installWithProxy` in component.ts, `compileCache` in vue/src/index.ts, and `globalVersion`/`activeSub` in reactivity are all process-global.
- The `ecosystem-ci-trigger` workflow accepts `/ecosystem-ci run` from issue comments with `issues: write`/`pull-requests: write` permissions, mitigated by a `checkMembershipForUser({ org: 'vuejs' })` gate that throws for non-members — the gate is the only thing between a comment and a triggered pipeline.
Extension points
Extensibility is designed in at four levels. Platform level: `createRenderer(options)` lets anyone build a custom renderer from the same core — `runtime-test` is the in-repo proof. Compile level: `nodeTransforms`/`directiveTransforms` are concatenated after the built-in presets, and compiler-dom/compiler-ssr are themselves just different preset compositions. App level: `app.use`/`app.component`/`app.directive`/`app.mixin` plus `app.config` hooks. Type level: declaration merging into `@vue/runtime-core` interfaces. Adding a package means adding a directory with a `buildOptions` block — `scripts/aliases.js` and `scripts/build.js` discover it automatically.
- `createRenderer<HostNode, HostElement>(options)` is the platform extension point; `createHydrationRenderer` is a separate export specifically so hydration stays tree-shakable, and both delegate to the same `baseCreateRenderer`.
- A renderer is defined entirely by the `RendererOptions` object; runtime-dom composes it as `extend({ patchProp }, nodeOps)`, and `packages/runtime-test` exists as a complete alternative host that renders plain JS objects for assertions.
- `baseCompile` appends `options.nodeTransforms` after the built-in preset and merges `options.directiveTransforms` over the built-in `on`/`bind`/`model` map, so third-party compiler plugins plug in without forking the preset.
- `createStructuralDirectiveTransform` in transform.ts is the factory for writing new structural directives in the style of v-if/v-for, and `traverseNode` is the hook point where each `nodeTransforms[i]` runs and may return an on-exit callback.
- compiler-dom's `DOMNodeTransforms` and `DOMDirectiveTransforms` are exported arrays/records, explicitly so compiler-ssr can reuse them to generate vnode fallback branches — the comment on `ignoreSideEffectTags` documents why it is kept out of that list.
- The app-level registries are `context.components`, `context.directives`, `context.provides`, `context.mixins` and the three WeakMap caches (`optionsCache`, `propsCache`, `emitsCache`) created by `createAppContext()`; `app.component`/`app.directive` are thin registrars over the first two with dev-time name validation.
- The plugin interface is `Plugin<Options> = FunctionPlugin | ObjectPlugin` with an `install(app, ...options)` signature; `app.onUnmount(cb)` gives plugins a teardown hook invoked through `callWithAsyncErrorHandling` with `ErrorCodes.APP_UNMOUNT_CLEANUP`.
- `AppConfig` exposes `errorHandler`, `warnHandler`, `globalProperties`, `optionMergeStrategies`, `compilerOptions` and `performance` as the supported runtime customisation surface, with `app.config` itself made non-replaceable via a warning setter.
- TypeScript extension is via declaration merging into named interfaces — runtime-dom itself does this for `RefUnwrapBailTypes`, `AllowedAttrs`, `GlobalComponents` and `GlobalDirectives`, which is the same mechanism router/store libraries use.
- `HydrationStrategyFactory` is an exported type and the four built-in strategies are plain factory functions, so a userland strategy can be passed to `defineAsyncComponent({ hydrate })` without touching core.
- `registerRuntimeCompiler(_compile)` is the documented seam for swapping the runtime compiler, and `isRuntimeOnly()` is derived from whether it has been called — that single boolean drives the dev-only `compilerOptions` deprecation warnings.
- `registerTS(loadTS)` and `invalidateTypeCache` are exported from compiler-sfc as the integration seam for bundlers to supply their own TypeScript instance and bust the type cache on HMR, with a hand-written error message when the module cannot be resolved.
Contributor handoff
Read the contributing guide's Project Structure and Package Dependencies sections before touching anything — the import rules there are enforced by both eslint and the release process. Expect ES2016-target lint restrictions that will reject idiomatic modern TypeScript, and expect every dev-only behaviour to be guarded by a `__DEV__` flag that must tree-shake cleanly. The fastest verification loop is `pnpm test-unit <package>`; the slowest and most CI-relevant are `pnpm check`, `pnpm test-dts` and `node scripts/verify-treeshaking.js`. Never reach across packages with relative paths, and never add a runtime import to a compiler package.
- Read first, in order: `.github/contributing.md` (scripts + project structure), `package.json` (the script surface), `vitest.config.ts` (which project your test will land in), and `scripts/aliases.js` (how `@vue/*` resolves in tests and builds).
- The eslint config bans object rest/spread, `await`, optional chaining and `const enum` repo-wide with explicit rationale messages (ES2016 output target, esbuild helper bloat, enum inlining); use `extend` from `@vue/shared` instead of object spread.
- `no-restricted-globals` is scoped by directory: DOM globals are banned everywhere except `packages/{vue,vue-compat,runtime-dom}` and shared; Node globals are banned in DOM packages; `import-x/no-nodejs-modules` allows bare (non-`node:`) builtins only under `packages/compiler-sfc/src/**`.
- Safe edit boundary: a change confined to one package's `src` plus its `__tests__` is verifiable with `pnpm test-unit <pkg>`; anything touching `renderer.ts`, `component.ts`, `scheduler.ts` or `vnode.ts` needs the full `pnpm test-unit` plus `pnpm test-e2e` because runtime-test, SSR and hydration all ride on them. (inferred)
- Trap: any new public export must also be re-exported at the package index — direct relative imports across package boundaries are forbidden, and a non-type import from package B requires B to be listed in A's package.json dependencies because the packages are externalised in ESM/CJS builds.
- Trap: commit messages are validated by a regex in `scripts/verify-commit.js` limited to a fixed type list with a ≤50-character subject; a non-conforming message fails the commit-msg hook outright.
- Trap: the pre-commit hook runs `pnpm lint-staged && pnpm check`, so a full `tsc --incremental --noEmit` over the whole repo runs on every commit — type errors in unrelated packages will block an otherwise-clean change.
- Trap: `tsconfig.json` sets `isolatedDeclarations: true`, which means exported functions and consts need explicit return/value type annotations — this is why the codebase writes `export const version: string = __VERSION__` and `export function initDev(): void`.
- Convention: dev-only code goes inside `if (__DEV__)` or `__DEV__ && ...` rather than behind a runtime boolean, and new warnings should be asserted in tests with the custom `toHaveBeenWarned` matchers.
- Convention: compile-time optional features must be wrapped in the corresponding flag — `__FEATURE_OPTIONS_API__` for `app.mixin` and `applyOptions`, `__FEATURE_SUSPENSE__` for Suspense dispatch, `__COMPAT__` for Vue-2 behaviour — or the feature will not tree-shake and `verify-treeshaking.js` may fail.
- Tests to run before opening a PR, mirroring CI exactly: `pnpm run test-unit`, `pnpm run test-e2e`, `pnpm run lint`, `pnpm run format-check`, `pnpm run check`, `pnpm run test-dts`.
- To iterate on compiler output interactively use `nr dev-compiler` (template-explorer at `localhost:3000/packages-private/template-explorer/local.html`, opened by `nr open`); for full SFC behaviour use `nr dev-sfc`, which runs `pre-dev-sfc.js` and falls back to `build-all-cjs`.
Languages
| Language | Files | Share of source |
|---|---|---|
| TypeScript | 489 | 91% |
| JavaScript | 36 | 7% |
| Markdown | 31 | docs/config |
| JSON | 30 | docs/config |
| HTML | 25 | docs/config |
| YAML | 12 | docs/config |
| Vue | 11 | 2% |
| CSS | 1 | docs/config |
Start reading here
Probable source entrypoint: packages/ — inferred from the directory tree and manifest, not the upstream docs.
Source roots
packages/ (569 files)
Source layout — packages/
| Path | Files |
|---|---|
packages/compiler-core/ | 69 |
packages/compiler-sfc/ | 69 |
packages/runtime-dom/ | 37 |
packages/vue/ | 46 |
packages/vue-compat/ | 22 |
Repository structure
| Top-level path | Files |
|---|---|
packages/ | 569 |
packages-private/ | 72 |
.github/ | 17 |
scripts/ | 12 |
changelogs/ | 5 |
.well-known/ | 1 |
Docs found: README.md
Package
package.json — v3.5.43, 52 declared dependencies.
@babel/parser@babel/types@rollup/plugin-alias@rollup/plugin-commonjs@rollup/plugin-json@rollup/plugin-node-resolve@rollup/plugin-replace@swc/core@types/hash-sum@types/node@types/semver@types/serve-handler
CI & config entrypoints
README.md— Repository readmepackages-private/sfc-playground/vite.config.ts— Vite configpackages-private/sfc-playground/src/download/template/vite.config.js— Vite configpackages-private/vite-debug/vite.config.ts— Vite config.github/workflows/autofix.yml— GitHub Actions workflow.github/workflows/release.yml— GitHub Actions workflow.github/workflows/size-report.yml— GitHub Actions workflow.github/workflows/test.yml— GitHub Actions workflow.github/workflows/ecosystem-ci-trigger.yml— GitHub Actions workflow.github/workflows/lock-closed-issues.yml— GitHub Actions workflow
Structural health signals
Average score 7.1/10 across 96 analyzed source files; 23 hotspots surfaced.
packages/runtime-core/src/renderer.ts— 1698 lines, score 5 (Large file, Deep nesting)packages/runtime-core/src/componentOptions.ts— 1327 lines, score 5 (Large file, Deep nesting)packages/compiler-core/src/parser.ts— 1079 lines, score 5 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
4ab865a848a12026-09-18 — fix(suspense): skip rendering async components unmounted while pending897924c4ff792026-09-17 — ci(issues): remove automatic issue closing workflow (#15557)5be58b4c475c2026-09-17 — release: v3.5.43a0b61aac0e7e2026-09-17 — chore: remove and ignore generated git hooks2fde3239ca3c2026-09-17 — fix(compiler-sfc): ignore comment delimiters inside strings and url() when parsing css vars (#15548)