packages/nuxt/src/migrations/update-22-2-0/files/ai-instructions-for-nuxt-4.md
This document provides instructions for an AI Agent to assist with migrating Nuxt 3 projects to Nuxt 4.
Before starting the migration, run these commands to understand the scope:
# List all Nuxt projects in the workspace
nx show projects --with-target build | xargs -I {} sh -c 'cat {}/nuxt.config.ts 2>/dev/null && echo "Project: {}"' | grep -B1 "Project:"
# Find all files that may need updates
find . -name "*.vue" -o -name "*.ts" | head -50
Search Pattern: Check nuxt.config.ts for srcDir configuration
Nuxt 4 introduces a new default directory structure using app/ instead of src/.
If adopting new structure:
// Before (Nuxt 3 with srcDir)
export default defineNuxtConfig({
srcDir: 'src',
});
// After (Nuxt 4 - remove srcDir, move files to app/)
export default defineNuxtConfig({
// srcDir removed - app/ is now the default
});
Action Items:
src/ contents to app/ directoryserver/, public/, layers/, modules/ at project rootsrcDir from nuxt.config.tsOR to keep existing structure:
export default defineNuxtConfig({
srcDir: '.',
dir: { app: 'app' },
});
generate ConfigurationSearch Pattern: grep -r "generate:" --include="nuxt.config.ts"
// Before
export default defineNuxtConfig({
generate: {
exclude: ['/admin'],
routes: ['/sitemap.xml'],
},
});
// After
export default defineNuxtConfig({
nitro: {
prerender: {
ignore: ['/admin'],
routes: ['/sitemap.xml'],
},
},
});
Search Pattern: Check tsconfig.json files
Nuxt 4 sets noUncheckedIndexedAccess: true by default.
To override if needed:
export default defineNuxtConfig({
typescript: {
tsConfig: {
compilerOptions: {
noUncheckedIndexedAccess: false,
},
},
},
});
null to undefinedSearch Pattern: grep -rn "!== null\|=== null" --include="*.vue" --include="*.ts"
// Before (Nuxt 3)
const { data } = await useAsyncData('key', () => fetch('/api/data'));
if (data.value !== null) {
/* ... */
}
// After (Nuxt 4)
const { data } = await useAsyncData('key', () => fetch('/api/data'));
if (data.value !== undefined) {
/* ... */
}
Automation available:
npx codemod@latest nuxt/4/default-data-error-value
getCachedData Context ParameterSearch Pattern: grep -rn "getCachedData" --include="*.vue" --include="*.ts"
// Before
getCachedData: (key, nuxtApp) => cachedData[key];
// After
getCachedData: (key, nuxtApp, ctx) => {
// ctx.cause: 'initial' | 'refresh:hook' | 'refresh:manual' | 'watch'
if (ctx.cause === 'refresh:manual') return undefined;
return cachedData[key];
};
Search Pattern: grep -rn "useAsyncData\|useFetch" --include="*.vue" --include="*.ts"
Data from useAsyncData/useFetch is now shallowRef (not deep reactive).
// If deep reactivity is needed:
const { data } = useFetch('/api/test', { deep: true });
Automation available:
npx codemod@latest nuxt/4/shallow-function-reactivity
dedupe Boolean ValuesSearch Pattern: grep -rn "dedupe: true\|dedupe: false" --include="*.vue" --include="*.ts"
// Before
refresh({ dedupe: true });
refresh({ dedupe: false });
// After
refresh({ dedupe: 'cancel' });
refresh({ dedupe: 'defer' });
Automation available:
npx codemod@latest nuxt/4/deprecated-dedupe-value
Search Pattern: Check dynamic route files [*.vue in pages/
// Before (unsafe for dynamic routes)
const { data } = await useAsyncData(async () =>
$fetch(`/api/page/${route.params.slug}`)
);
// After (safe - key includes slug)
const { data } = await useAsyncData(route.params.slug, async () =>
$fetch(`/api/page/${route.params.slug}`)
);
Search Pattern: grep -rn "hid:\|vmid:\|children:\|body:" --include="*.vue" --include="*.ts"
// Before
useHead({
meta: [{ name: 'description', hid: 'description', content: 'My page' }],
script: [{ children: 'console.log("hello")' }],
});
// After
useHead({
meta: [{ name: 'description', content: 'My page' }],
script: [{ innerHTML: 'console.log("hello")' }],
});
Search Pattern: Check test files using findComponent and templates with <KeepAlive>
// Component in SomeFolder/MyComponent.vue
// Before: findComponent({ name: 'MyComponent' })
// After: findComponent({ name: 'SomeFolderMyComponent' })
To disable if needed:
export default defineNuxtConfig({
experimental: {
normalizeComponentNames: false,
},
});
Search Pattern: grep -rn "route.meta.name\|route.meta.path" --include="*.vue" --include="*.ts"
// Before
const name = route.meta.name;
// After
const name = route.name;
These flags are now hardcoded and cannot be configured:
experimental.treeshakeClientOnly → always trueexperimental.configSchema → always trueexperimental.polyfillVueUseHead → always falseexperimental.respectNoSSRHeader → always falseAction Items:
nuxt.config.ts if presenterror.dataSearch Pattern: grep -rn "JSON.parse.*error.data\|error.data.*JSON.parse" --include="*.vue" --include="*.ts"
// Before
const data = JSON.parse(error.data);
// After (data is already parsed)
const data = error.data;
builder:watchSearch Pattern: Check custom modules using builder:watch hook
import { relative, resolve } from 'node:fs';
nuxt.hook('builder:watch', async (event, path) => {
// Convert to relative path for backward/forward compatibility
path = relative(nuxt.options.srcDir, resolve(nuxt.options.srcDir, path));
});
Search Pattern: Check modules using addTemplate with EJS syntax
// Before (using lodash template)
addTemplate({
fileName: 'plugin.js',
src: './runtime/plugin.ejs',
});
// After (using getContents)
import { template } from 'es-toolkit/compat';
addTemplate({
fileName: 'plugin.js',
getContents({ options }) {
const contents = readFileSync('./runtime/plugin.ejs', 'utf-8');
return template(contents)({ options });
},
});
Note: This section only applies if your workspace uses ESLint flat config (
eslint.config.js,eslint.config.mjs, oreslint.config.cjs). If you're using legacy.eslintrc.json, no changes are required.
createConfigForNuxtSearch Pattern: Check for eslint.config.js, eslint.config.mjs, or eslint.config.cjs in Nuxt project directories
For workspaces using ESLint flat config, Nuxt 4 requires updating to @nuxt/eslint-config version ^1.10.0 and using createConfigForNuxt from @nuxt/eslint-config/flat.
Before (Nuxt 3 flat config):
import baseConfig from '../../eslint.config.mjs';
export default [
...baseConfig,
{
files: ['**/*.vue'],
languageOptions: {
parserOptions: { parser: '@typescript-eslint/parser' },
},
},
{
ignores: ['.nuxt/**', '.output/**', 'node_modules'],
},
];
After (Nuxt 4 flat config - eslint.config.mjs):
import { createConfigForNuxt } from '@nuxt/eslint-config/flat';
import baseConfig from '../../eslint.config.mjs';
export default createConfigForNuxt({
features: {
typescript: true,
},
})
.prepend(...baseConfig)
.append(
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
rules: {},
},
{
ignores: ['.nuxt/**', '.output/**', 'node_modules'],
}
);
For CJS (eslint.config.cjs):
const { createConfigForNuxt } = require('@nuxt/eslint-config/flat');
const baseConfig = require('../../eslint.config.cjs');
module.exports = createConfigForNuxt({
features: {
typescript: true,
},
})
.prepend(...baseConfig)
.append(
{
files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
rules: {},
},
{
ignores: ['.nuxt/**', '.output/**', 'node_modules'],
}
);
Action Items (Flat Config Only):
@nuxt/eslint-config to ^1.10.0 in package.jsoncreateConfigForNuxtfeatures.typescript: true option for TypeScript support@typescript-eslint/parser from devDependencies (handled automatically).prepend() for base configs and .append() for project-specific rules/ignoresThe createConfigForNuxt function returns a chainable config builder:
features.typescript: true - Enables TypeScript support with proper Vue file parsing.prepend(...configs) - Adds configs at the beginning (useful for workspace base configs).append(...configs) - Adds configs at the end (for project-specific rules and ignores)After completing the migration, run these commands:
# 1. Install updated dependencies
npm install
# 2. Run Nuxt prepare
npx nuxi prepare
# 3. Type check
npx nuxi typecheck
# 4. Build the application
nx build <project-name>
# 5. Run tests
nx test <project-name>
# 6. Start dev server to verify
nx serve <project-name>
Nuxt provides codemods to automate many changes:
# Run the full migration recipe
npx [email protected] nuxt/4/migration-recipe
# Or run individual codemods:
npx codemod@latest nuxt/4/file-structure
npx codemod@latest nuxt/4/default-data-error-value
npx codemod@latest nuxt/4/shallow-function-reactivity
npx codemod@latest nuxt/4/deprecated-dedupe-value
npx codemod@latest nuxt/4/template-compilation-changes
npx codemod@latest nuxt/4/absolute-watch-path
Solution: This is expected behavior - SPA loading template now renders outside #__nuxt. To revert:
experimental: {
spaLoadingTemplateLocation: 'within';
}
Solution: Only component CSS is inlined by default. To inline all CSS:
features: {
inlineStyles: true;
}
Solution: Filter unwanted middleware:
hooks: {
'app:resolve'(app) {
app.middleware = app.middleware.filter(mw => !/\/index\.[^/]+$/.test(mw.path))
}
}
# Find all Vue files
find . -name "*.vue" -not -path "./node_modules/*"
# Find all nuxt config files
find . -name "nuxt.config.*" -not -path "./node_modules/*"
# Find composables using data fetching
grep -rn "useAsyncData\|useFetch\|getCachedData" --include="*.vue" --include="*.ts" | grep -v node_modules
nx build and nx test after each major change