vuejs/core
Patterns and conventions
Vue's codebase has a small number of conventions that show up in nearly every file. Knowing them up front makes the source easier to read and avoids reviewer churn.
Build flags
Build flags are global identifiers that the bundler replaces with literals at compile time. They are declared in scripts/setup-vitest.ts for tests and in rollup.config.js for production builds.
__DEV__—truein dev,falsein prod. Wraps every warning, devtools hook, and developer-only validation.__BROWSER__— true for browser builds, false for Node SSR builds.__SSR__— true when the build needs to interop with@vue/server-renderer. Gates thessrUtilsexport and hydration code.__GLOBAL__/__ESM_BUNDLER__/__ESM_BROWSER__/__CJS__— exactly one is true depending on the target format.__COMPAT__— only true in thevue-compatbuild.__VERSION__— string literal injected frompackage.json.- User-facing:
__VUE_OPTIONS_API__,__VUE_PROD_DEVTOOLS__,__VUE_PROD_HYDRATION_MISMATCH_DETAILS__. Configured by the bundler. Seereference/configuration.
The cardinal rule: dev-only code goes inside if (__DEV__). Prod builds will tree-shake the entire branch.
if (__DEV__ && someInvariantHolds === false) {
warn('explanatory message');
}Anything that does not follow this pattern (a runtime check that assigns a flag for later use, for example) bloats the prod bundle and will be caught in review or by the size-report bot.
NOOP and friends
packages/shared/src/general.ts exports tiny utilities that show up everywhere:
NOOP—() => {}. Used as a default for optional callbacks and as the prod-build replacement for dev-only functions:export const warn = (__DEV__ ? _warn : NOOP) as typeof _warn.EMPTY_OBJ—Object.freeze({})in dev,{}in prod. Used as default props/slots/attrs to avoid allocating a new object per component.EMPTY_ARR—Object.freeze([])in dev,[]in prod.extend = Object.assign— pre-aliased so the minifier can rename it to a single character.hasChanged(value, oldValue)— usesObject.is, accounting forNaN.
The pattern is consistent: dev-mode behavior never increases prod size, and frequent identifiers are aliased once at module scope.
Tree-shakable lazy initialization
When something is heavy or platform-specific, it gets lazily created so the rest of the package can be imported without paying for it. Example from packages/runtime-dom/src/index.ts:
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer;
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
);
}This is why importing reactivity from Vue does not pull in the renderer.
The same pattern appears for hydration (ensureHydrationRenderer), compat utilities, and SSR directive initialization.
Internal vs public API
The runtime-core index file ends with two large blocks tagged // Internal API and // 2.x COMPAT. These exports follow a hard convention:
- Internal exports are wrapped in a JSDoc
/** @internal */comment. They are subject to change without notice between versions and user code should not import them. - Compat exports are guarded by
__COMPAT__so they only exist in the compat build.
When adding a new export, decide which bucket it lives in and place it in the matching section of the file. The order of exports inside the index files is meaningful — they are the public contract of the package.
Compiler/runtime separation
Two rules from .github/contributing.md (verbatim):
- Compiler packages should not import items from the runtime, and vice versa. If something needs to be shared, it goes into
@vue/shared. - Never use direct relative paths when importing items from another package. Export it in the source package and import it via the package name (
@vue/runtime-core). Path aliases inscripts/aliases.jsandtsconfig.json#pathsmake this work in dev.
A subtle related rule: some @vue/shared helpers are compiler-only (isHTMLTag, isSVGTag, etc.) and should never be reached from runtime code. Importing them from the runtime side accidentally pulls a long whitelist into every browser bundle.
Patch flags and shape flags
The compiler annotates vnodes with two bitmasks:
PatchFlags(packages/shared/src/patchFlags.ts) — what kind of update this vnode can have. The renderer uses these to skip work: aTEXTflag means only the text content needs diffing;STABLE_FRAGMENTmeans the children list has constant order;HOISTEDmeans the vnode is module-scope and never changes.ShapeFlags(packages/shared/src/shapeFlags.ts) — what this vnode is.ELEMENT,STATEFUL_COMPONENT,FUNCTIONAL_COMPONENT,TEXT_CHILDREN,ARRAY_CHILDREN,SLOTS_CHILDREN,TELEPORT,SUSPENSE,COMPONENT_KEPT_ALIVE,COMPONENT_SHOULD_KEEP_ALIVE.
When in doubt, check vnode.ts in runtime-core to see how flags are computed and how the renderer branches on them.
Error handling
User-supplied callbacks (event handlers, lifecycle hooks, watchers, render functions) are wrapped in callWithErrorHandling / callWithAsyncErrorHandling from packages/runtime-core/src/errorHandling.ts. The wrapper:
- Catches synchronous and Promise-rejection errors.
- Walks up the parent chain calling each
errorCapturedhook. - Falls back to
app.config.errorHandlerand finally the console.
The ErrorCodes enum in the same file gives each call site a stable numeric tag (e.g., ErrorCodes.COMPONENT_UPDATE). The matching ErrorTypeStrings map provides the human-readable label, but is included only in dev/ESM-bundler/CJS builds — hence the conditional export at the bottom of runtime-core/src/index.ts.
Const enum inlining
Vue uses const enum heavily for hot-path bitmasks (ShapeFlags, PatchFlags, ErrorCodes, EffectFlags, SchedulerJobFlags). scripts/inline-enums.js rewrites these to numeric literals during the build because:
- esbuild does not support
const enuminlining out of the box. - Inlining produces faster code (literal compares instead of property reads) and smaller minified output.
When adding a new enum, follow the existing pattern and don't introduce a regular enum — it would cost a runtime object.
Code style notes
- Single quotes, no semicolons. Prettier enforces this; pre-commit fixes it automatically.
- TypeScript types prefer
typealiases for unions and intersections;interfacefor object shapes that may be augmented (RefUnwrapBailTypes,GlobalComponents,GlobalDirectives). - Internal helpers prefer compact names (
def,extend,isFn) over verbose ones. Public API is the opposite —defineAsyncComponent,useTemplateRef. - File names in the runtime use lower camelCase (
apiCreateApp.ts,componentEmits.ts); built-in components use PascalCase to match their public name (Suspense.ts,Teleport.ts); compiler files are descriptive (tokenizer.ts,parser.ts,transformElement.ts). - Public types live next to their implementation and are re-exported by the package index in a single big
export type { ... } from './...'block.
For more, see tooling for build scripts and debugging for the iterations they support.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.