Senior craft

Rollout and breaking changes

What counts as breaking for a layer and for a module, the deprecate-warn-codemod-remove sequence, and how to roll a change out to twenty apps without an incident.

"How would you roll out a breaking change to twenty apps?" is the most reliable senior question in a tooling interview, because the answer reveals whether you have ever had consumers. The short version: you do not ship a breaking change, you run a migration.

Know — what counts as breaking

For a layer: renaming or removing a component, composable or util; removing or renaming an app.config key, or changing a default in a way that alters rendering; removing or renaming a runtimeConfig key; removing a page, route or layout; removing a module from the preset that apps relied on; raising the minimum Nuxt version; changing the shape of a type consumers extend.

For a module: renaming or removing an option; changing a default; removing an injected runtime API, hook or alias; raising the minimum Nuxt version; changing the emitted types in a way that fails nuxt typecheck in consuming apps.

Not breaking (but still worth a changelog line): adding an optional option, adding a component with a new prefixed name, adding a hook, widening a type.

Know — the sequence

  1. Decide with data. The adoption report says who uses the thing. If one app uses it, talk to that team instead of running a process.
  2. Deprecate in a minor. The old name keeps working, delegates to the new one, and warns in development only with a message that names the replacement and links the migration note.
  3. Write the migration note before the code. Consumer's point of view: what to change, what it looks like before and after, what breaks if they do nothing, and by when.
  4. Codemod when a rename touches many files. A short jscodeshift or regex script in the repository beats twenty teams doing the same edit by hand, and it turns a two-hour task into a reviewed pull request.
  5. Canary one app. Usually the platform team's own. Then a second, unrelated one.
  6. Announce with a support window. "Old name warns in 2.4, removed in 3.0, which we expect in Q1; we support 2.x until then."
  7. Remove in a major, once adoption data shows nobody is on the old path. If someone still is, the deadline was aspirational, not real.

How it works

The deprecation shim for a renamed layer component:

layers/base/app/components/Team/PrimaryButton.vue
<script setup lang="ts">
// Deprecated in 2.4, removed in 3.0. Use <TeamButton variant="primary">.
import TeamButton from './Button.vue'

// Dev-only: never add console noise to a consumer's production build.
if (import.meta.dev) {
  console.warn('[nuxt-layer-base] <TeamPrimaryButton> is deprecated and will be removed in 3.0. Use <TeamButton variant="primary">. Migration: https://git.internal/layers/base/MIGRATION.md#2-4')
}
</script>

<template>
  <TeamButton variant="primary" v-bind="$attrs">
    <slot />
  </TeamButton>
</template>

The equivalent for a renamed module option, which must keep working and keep type-checking:

src/module.ts
import { defineNuxtModule, useLogger } from '@nuxt/kit'

export interface ModuleOptions {
  /** @deprecated since 2.4, use `analytics.enabled`. Removed in 3.0. */
  trackingEnabled?: boolean
  analytics: { enabled: boolean }
}

export default defineNuxtModule<ModuleOptions>({
  meta: { name: 'nuxt-team-toolkit', configKey: 'toolkit' },
  defaults: { analytics: { enabled: false } },
  setup(options) {
    const logger = useLogger('toolkit')
    if (options.trackingEnabled !== undefined) {
      logger.warn('`toolkit.trackingEnabled` is deprecated and will be removed in 3.0; use `toolkit.analytics.enabled`.')
      // Old option still wins if the new one was not set, so nobody breaks today.
      options.analytics.enabled ??= options.trackingEnabled
    }
  },
})

The migration note consumers actually read:

MIGRATION.md
## 2.4 → 3.0

### `<TeamPrimaryButton>` removed
**Why** — one button component with a `variant` prop instead of five near-duplicates.
**Do** — replace `<TeamPrimaryButton>` with `<TeamButton variant="primary">`.
**Codemod**`npx @team/layer-codemods primary-button .` (reviews as a normal PR).
**If you do nothing** — the component is gone in 3.0 and the build fails with "Failed to resolve component".
**Deadline** — 3.0 ships in Q1; 2.x is supported until then.
Gotcha· A warning nobody sees

console.warn in a server-rendered component prints into the server log of a deployed app, where no developer is looking, and into production logs where it is noise. Guard with import.meta.dev, or emit the warning at build time from the module with useLogger, which lands in the terminal of the person who can act on it.

Gotcha· Removing on schedule instead of on evidence

A support window is a promise, not a countdown. If the adoption report shows three apps still on the old path on removal day, removing it converts your migration into three incidents. Either extend and escalate, or do the migration in their repositories yourself.

Exercise

Exercise
  • Rename a public component in your layer using the shim pattern. Ship it as a minor, write the CHANGELOG entry and the migration note, then remove it in a local "3.0" branch and watch what a consumer's build does.
  • Write a five-line codemod for that rename and run it against a fixture app.
  • Write the announcement message you would send the consuming teams. Three sentences: what, when, what they must do.

Be able to say

Be able to say· How do you roll out a breaking change to twenty apps?

"I do not ship it as a breaking change, I run a migration. First I check the adoption report to see who actually uses the thing, because if it is one team this is a conversation, not a process. Then a minor release where the old path still works and warns in development with a message naming the replacement, a migration note written from the consumer's side, and a codemod if the rename touches many files. I canary it in one app, announce a support window, watch adoption, and only remove it in the next major once the data says nobody is on the old path. Removing on a date instead of on evidence is how a migration becomes three incidents."