Open-Source Wikis

/

Vue.js

/

How to contribute

/

Patterns and conventions

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__true in dev, false in 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 the ssrUtils export and hydration code.
  • __GLOBAL__ / __ESM_BUNDLER__ / __ESM_BROWSER__ / __CJS__ — exactly one is true depending on the target format.
  • __COMPAT__ — only true in the vue-compat build.
  • __VERSION__ — string literal injected from package.json.
  • User-facing: __VUE_OPTIONS_API__, __VUE_PROD_DEVTOOLS__, __VUE_PROD_HYDRATION_MISMATCH_DETAILS__. Configured by the bundler. See reference/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_OBJObject.freeze({}) in dev, {} in prod. Used as default props/slots/attrs to avoid allocating a new object per component.
  • EMPTY_ARRObject.freeze([]) in dev, [] in prod.
  • extend = Object.assign — pre-aliased so the minifier can rename it to a single character.
  • hasChanged(value, oldValue) — uses Object.is, accounting for NaN.

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):

  1. Compiler packages should not import items from the runtime, and vice versa. If something needs to be shared, it goes into @vue/shared.
  2. 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 in scripts/aliases.js and tsconfig.json#paths make 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: a TEXT flag means only the text content needs diffing; STABLE_FRAGMENT means the children list has constant order; HOISTED means 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:

  1. Catches synchronous and Promise-rejection errors.
  2. Walks up the parent chain calling each errorCaptured hook.
  3. Falls back to app.config.errorHandler and 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 enum inlining 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 type aliases for unions and intersections; interface for 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.

Patterns and conventions – Vue.js wiki | Factory