How to Fix: Codemod upgrade script does not consider the absence of the “dev” script in the package.json
The upgrade codemod breaks when package.json has no dev script because it assumes that script always exists.
If you run the provided reproduction and execute npx @next/codemod@canary upgrade latest, the upgrade flow can fail while trying to inspect or rewrite project scripts. The core problem is simple: the codemod logic treats scripts.dev as mandatory, but many valid Node.js projects either rename it, omit it, or rely on another local workflow entirely.
Understanding the Root Cause
In a typical Next.js project, package.json often contains:
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
However, that structure is a convention, not a requirement. A project may legitimately have:
"scripts": {
"build": "next build",
"start": "next start"
}
or even custom names such as:
"scripts": {
"serve": "next dev",
"build": "next build"
}
The failing codemod path likely performs one of these unsafe operations:
- Reads
scripts.devwithout checking whetherscriptsexists. - Applies string operations to
scripts.deveven when it isundefined. - Assumes the project should always be patched based on the dev script rather than checking all relevant scripts defensively.
That means the bug is not in the app itself. The bug lives in the upgrade script’s assumption model. A codemod should treat package.json as user-owned configuration and handle missing fields gracefully.
Technically, the correct behavior is one of the following:
- If
devis missing, skip thedev-specific transformation. - If the migration only needs to update
devwhen present, guard access with optional checks. - If the migration needs to detect
next dev, scan all scripts rather than hardcodingscripts.dev.
Step-by-Step Solution
The safest fix in the codemod is to add a guard before reading or rewriting scripts.dev.
1. Inspect the current package.json shape
Affected projects often look like this:
{
"name": "my-app",
"private": true,
"scripts": {
"build": "next build",
"start": "next start"
}
}
This is valid. The absence of dev should not cause a crash.
2. Patch the codemod logic to handle missing scripts safely
If the upgrade script currently does something like this:
const devScript = pkg.scripts.dev;
if (devScript.includes('next dev')) {
pkg.scripts.dev = transformDevScript(devScript);
}
replace it with defensive access:
const scripts = pkg.scripts || {};
const devScript = scripts.dev;
if (typeof devScript === 'string' && devScript.includes('next dev')) {
scripts.dev = transformDevScript(devScript);
}
pkg.scripts = scripts;
If the code may need to work even when scripts is entirely absent, this pattern prevents undefined property access.
3. Use a more robust script detection strategy
If the real goal is to find any script invoking next dev, do not rely only on the dev key:
const scripts = pkg.scripts || {};
for (const [name, value] of Object.entries(scripts)) {
if (typeof value === 'string' && value.includes('next dev')) {
scripts[name] = transformDevScript(value);
}
}
This makes the codemod more resilient for teams with custom script names.
4. Add a no-op path when nothing needs updating
A codemod should complete successfully even if there is nothing to change:
const scripts = pkg.scripts || {};
const hasDevScript = typeof scripts.dev === 'string';
if (!hasDevScript) {
console.log('No dev script found. Skipping dev script migration.');
return;
}
This is especially important for automated upgrade tooling, where skipping safely is better than aborting the entire upgrade.
5. Validate with the reproduction case
Re-run the upgrade against the reproduction project after patching the guard logic:
npx @next/codemod@canary upgrade latest
Expected result:
- The script should not crash.
- The upgrade should continue normally.
- If no
devscript exists, the tool should skip that mutation.
6. Recommended production-safe implementation
function updatePackageJson(pkg) {
const scripts = pkg.scripts || {};
if (typeof scripts.dev === 'string') {
if (scripts.dev.includes('next dev')) {
scripts.dev = transformDevScript(scripts.dev);
}
}
pkg.scripts = scripts;
return pkg;
}
If broader script migration is intended:
function updatePackageJson(pkg) {
const scripts = pkg.scripts || {};
for (const key of Object.keys(scripts)) {
const script = scripts[key];
if (typeof script === 'string' && script.includes('next dev')) {
scripts[key] = transformDevScript(script);
}
}
pkg.scripts = scripts;
return pkg;
}
Common Edge Cases
- No scripts object at all: Some packages omit
scriptsentirely. Accessingpkg.scripts.devwill throw unlesspkg.scriptsis defaulted first. - Non-string script values: While uncommon, malformed JSON or generated configs can produce unexpected values. Always verify
typeof script === 'string'. - Custom script names: Teams may use
serve,local, or workspace-specific names instead ofdev. - Monorepo package variance: In a workspace, some packages may be apps and others may be libraries. The codemod should avoid assuming every
package.jsonhas runnable Next.js scripts. - Script wrappers: A script may call another command like
cross-env NODE_ENV=development next dev. Naive exact-match checks can miss valid upgrade targets. - Chained commands: Scripts such as
turbo run devorpnpm --filter web devmay indirectly invoke Next.js. Decide whether the codemod should skip those or only handle directnext devusage.
FAQ
Why is missing dev considered valid in package.json?
Because scripts is fully user-defined. npm, pnpm, and Yarn do not require a dev key. It is only a common convention in app templates.
Should the codemod add a default dev script automatically?
Usually no. If the issue is only about upgrading safely, the better behavior is to skip missing optional fields rather than mutate project conventions unexpectedly.
What is the best long-term fix for maintainers?
Update the upgrade script to use defensive checks, treat absent fields as a no-op, and add tests covering package.json files with no dev script, no scripts object, and custom script names.
The key takeaway is that the codemod should be schema-tolerant. A missing dev script is not an invalid project state, so the upgrade path must detect that case and continue without failure.